diff --git a/.agents/LESSONS.md b/.agents/LESSONS.md index 5a30a4ca0f..ee6ef3b02e 100644 --- a/.agents/LESSONS.md +++ b/.agents/LESSONS.md @@ -5,9 +5,11 @@ Lessons accumulated across buffbench runs. Each lesson identifies what went wron ## 2025-10-21T02:19:38.224Z — add-sidebar-fades (257cb37) ### Original Agent Prompt + Enhance the desktop docs sidebar UX by adding subtle top/bottom gradient fades that appear based on scroll position and a thin, themed custom scrollbar. The fades should show when there’s overflow in that direction (top when not at the top, bottom when not at the bottom), be non-interactive, and update on initial render and during scroll. Apply the custom scrollbar styles via a CSS class and use it on the scrollable sidebar container. Preserve the current hash-based smooth scrolling behavior and leave the mobile Sheet implementation unchanged. ### Lessons + - **Issue:** Custom scrollbar only used -webkit selectors; Firefox shows default thick scrollbar. **Fix:** Add cross-browser styles: scrollbar-width: thin; scrollbar-color: hsl(var(--border)/0.6) transparent alongside -webkit rules. @@ -23,9 +25,11 @@ Enhance the desktop docs sidebar UX by adding subtle top/bottom gradient fades t ## 2025-10-21T02:24:18.953Z — validate-custom-tools (30dc486) ### Original Agent Prompt + Add schema-validated custom tool execution. Ensure the server validates custom tool inputs but forwards a sanitized copy of the original input (removing the end-of-step flag) to the client. In the SDK, parse custom tool inputs with the provided Zod schema before invoking the tool handler and update types so handlers receive fully parsed inputs. Keep built-in tool behavior and error handling unchanged. ### Lessons + - **Issue:** Server streamed tool_call with parsed input, not sanitized original; client sees schema-shaped payload instead of original minus cb_easp. **Fix:** In parseRawCustomToolCall, validate with Zod but return input as a clone of raw input with cb_easp removed; use that for toolCalls and onResponseChunk. @@ -41,9 +45,11 @@ Add schema-validated custom tool execution. Ensure the server validates custom t ## 2025-10-21T02:25:18.751Z — filter-system-history (456858c) ### Original Agent Prompt + Improve spawned agent context handling so that parent system messages are not forwarded. Update both sync and async spawn flows to pass conversation history to sub-agents without any system-role entries, and add tests covering includeMessageHistory on/off, empty history, and system-only history. Keep the overall spawning, validation, and streaming behavior unchanged. ### Lessons + - **Issue:** Tests asserted raw strings in the serialized history (e.g., 'assistant', '[]'), making them brittle to formatting changes. **Fix:** Parse the JSON portion of conversationHistoryMessage and assert on structured fields (roles, length), not string substrings. @@ -56,24 +62,23 @@ Improve spawned agent context handling so that parent system messages are not fo - **Issue:** Role presence was verified by substring checks ('assistant') instead of checking message.role, risking false positives. **Fix:** Assert on exact role fields ("role":"assistant") or, better, parse JSON and check objects’ role values. -- **Issue:** Initial sync test expected a non-standard empty array format ('[\n \n]'), requiring a later patch. +- **Issue:** Initial sync test expected a non-standard empty array format ('[\n \n]'), requiring a later patch. **Fix:** Use JSON.stringify semantics from the start or parse JSON and assert length === 0 to avoid format assumptions. ## 2025-10-21T02:26:14.756Z — add-spawn-perms-tests (257c995) ### Original Agent Prompt + Add comprehensive unit tests to verify that the spawn_agents tool enforces parent-to-child spawn permissions and that agent ID matching works across publisher, name, and version combinations. Include edge cases and mixed-success scenarios. Also make the internal matching helper importable so the tests can target it directly. Keep the handler logic unchanged; focus on exporting the helper and covering behavior via tests. ### Lessons + - **Issue:** Imported TEST_USER_ID from '@codebuff/common/constants' and AgentTemplate from '../templates/types' causing type/resolve errors. **Fix:** Use correct paths: TEST_USER_ID from '@codebuff/common/old-constants' and AgentTemplate from '@codebuff/common/types/agent-template'. - **Issue:** Omitted the 'agent template not found' scenario in handler tests, missing a key error path. **Fix:** Add a test where localAgentTemplates lacks the requested agent; assert the error message and no loopAgentSteps call. -- **Issue:** Reimplemented ProjectFileContext and MockWebSocket in tests instead of reusing shared utils. - **Fix:** Import mockFileContext and MockWebSocket from backend/src/__tests__/test-utils to avoid drift and boilerplate. - - **Issue:** Assertions tightly coupled to exact report header strings, making tests brittle to formatting changes. **Fix:** Assert via displayName-derived headers or use regex/contains on content while verifying loopAgentSteps calls for success. @@ -92,13 +97,14 @@ Add comprehensive unit tests to verify that the spawn_agents tool enforces paren ## 2025-10-21T02:27:58.739Z — extract-agent-parsing (998b585) ### Original Agent Prompt -Please consolidate agent ID parsing across the backend by introducing a shared util and updating the registry and spawn logic: + - Add a common parser that can handle both published and local agent IDs, and a strict parser that only passes when a publisher is present. - Update the agent registry to rely on the strict parser for DB lookups and to prefix with the default org when needed. - Update the spawn-agents handler to use the shared general parser, with guards for optional fields, so that unprefixed, prefixed, and versioned forms are all matched correctly against the parent’s spawnable agents. -Keep the existing registry cache behavior and spawn matching semantics the same, and make sure existing tests pass without modification. + Keep the existing registry cache behavior and spawn matching semantics the same, and make sure existing tests pass without modification. ### Lessons + - **Issue:** Put new parsers in agent-name-normalization.ts, conflating concerns and diverging from the repo’s dedicated parsing util pattern. **Fix:** Create common/src/util/agent-id-parsing.ts exporting parseAgentId + parsePublishedAgentId; import these in registry and spawn-agents. @@ -114,9 +120,11 @@ Keep the existing registry cache behavior and spawn matching semantics the same, ## 2025-10-21T02:29:20.144Z — enhance-docs-nav (26140c8) ### Original Agent Prompt + Improve the developer docs experience: make heading clicks update the URL with the section hash and smoothly scroll to the heading, and ensure back/forward navigation to hashes also smoothly scrolls to the right place. Then refresh the Codebuff vs Claude Code comparison and agent-related docs to match current messaging: add SDK/programmatic bullets, expand Claude-specific enterprise reasons, standardize the feature comparison table, streamline the creating/customizing agent docs with concise control flow and field lists, and move domain-specific customization examples out of the overview into the customization page. Keep styles and existing components intact while making these UX and content updates. ### Lessons + - **Issue:** copy-heading.tsx onClick handler misses a closing brace/paren, causing a TS/compile error. **Fix:** Run typecheck/format before commit and ensure onClick closes with '})'. Build locally to catch syntax errors. @@ -138,9 +146,11 @@ Improve the developer docs experience: make heading clicks update the URL with t ## 2025-10-21T02:30:15.502Z — match-spawn-agents (9f0b66d) ### Original Agent Prompt + Enable flexible matching for spawning subagents. When a parent agent spawns children, the child agent_type string may include an optional publisher and/or version. Update the spawn-agents handler so a child can be allowed if its identifier matches any of the parent’s spawnable agents by agent name alone, by name+publisher, by name+version, or by exact name+publisher+version. Export the existing agent ID parser and use it to implement this matching, while preserving all current spawning, validation, and streaming behaviors. ### Lessons + - **Issue:** Matching was too strict: name-only child failed when parent allowed had publisher/version. **Fix:** Use asymmetric match: if names equal, allow regardless of extra qualifiers on either side. @@ -165,9 +175,11 @@ Enable flexible matching for spawning subagents. When a parent agent spawns chil ## 2025-10-21T02:31:29.648Z — add-deep-thinkers (6c362c3) ### Original Agent Prompt + Add a family of deep-thinking agents that orchestrate multi-model analysis. Create one coordinator agent that spawns three distinct sub-thinkers (OpenAI, Anthropic, and Gemini) and synthesizes their perspectives, plus a meta-coordinator that can spawn multiple instances of the coordinator to tackle different aspects of a problem. Each agent should define a clear purpose, model, and prompts, and the coordinators should be able to spawn their sub-agents. Ensure the definitions follow the existing agent typing, validation, and spawn mechanics used across the project. ### Lessons + - **Issue:** Sub-thinkers rely on stepPrompt to call end_turn; no handleSteps to guarantee completion. **Fix:** Add handleSteps that yields STEP_ALL (or STEP then end_turn) to deterministically end each sub-thinker. @@ -187,7 +199,7 @@ Add a family of deep-thinking agents that orchestrate multi-model analysis. Crea **Fix:** Use template literals with .trim() for system/instructions/step prompts to keep style consistent. - **Issue:** Captured toolResult into unused vars (subResults/aspectResults), causing avoidable lint warnings. - **Fix:** Prefix unused bindings with _ or omit them entirely to keep code lint-clean from the start. + **Fix:** Prefix unused bindings with \_ or omit them entirely to keep code lint-clean from the start. - **Issue:** Coordinator synthesis depends solely on implicit instructions; no structured output path. **Fix:** Yield STEP_ALL and optionally switch to structured_output + set_output to enforce a concrete synthesis. @@ -195,13 +207,14 @@ Add a family of deep-thinking agents that orchestrate multi-model analysis. Crea ## 2025-10-21T02:33:02.024Z — add-custom-tools (212590d) ### Original Agent Prompt + Add end-to-end support for user-defined custom tools alongside the built-in tool set. Agents should be able to list custom tools by string name, the system should describe and document them in prompts, recognize their calls in streamed responses, validate their inputs, and route execution to the SDK client where the tool handler runs. Include options for tools that end the agent step, and support example inputs for prompt documentation. Update types, schemas, and test fixtures accordingly. ### Lessons + - **Issue:** CodebuffToolCall stays tied to ToolName; custom names break typing and casts to any in stream-parser/tool-executor. **Fix:** Broaden types to string tool names. Update CodebuffToolCall/clientTool schemas to accept custom names and map to runtime schemas. -- **Issue:** AgentTemplate lacks customTools in backend/types, yet code references AgentTemplate['customTools'] in strings.ts/tool-executor. **Fix:** Add customTools to AgentTemplate (record by name). Ensure assembleLocalAgentTemplates builds this map from agent defs. - **Issue:** convertJsonSchemaToZod used in common/src/templates/agent-validation.ts without import/impl; likely compile error. @@ -210,9 +223,6 @@ Add end-to-end support for user-defined custom tools alongside the built-in tool - **Issue:** customTools defined as array in dynamic-agent-template, but prompts expect a record (customTools[name]). **Fix:** Normalize to Record during validation. Store the record on AgentTemplate; use it everywhere. -- **Issue:** Changed sdk/src/client.ts for overrides, but actual routing uses sdk/src/websocket-client.ts handleToolCall. - **Fix:** Document that clients must implement handleToolCall for custom tools or extend websocket-client to dispatch overrides. - - **Issue:** Example inputs aren’t rendered in tool docs; requirement asked for example inputs in prompts. **Fix:** Enhance getToolsInstructions/getShortToolInstructions to render exampleInputs blocks under each tool description. @@ -222,13 +232,11 @@ Add end-to-end support for user-defined custom tools alongside the built-in tool - **Issue:** Loosened toolNames to string[] without validating built-ins vs custom; invalid names can slip silently. **Fix:** Validate toolNames: each must be built-in or exist in customTools. Emit clear validation errors with file context. -- **Issue:** Duplicate import of renderToolResults added in backend/src/tools/tool-executor.ts. **Fix:** Remove duplicate import and run the build/tests locally to catch such issues early. - **Issue:** processStreamWithTags autocompletes with cb_easp: true always; may invalidate non-end tools’ schemas. **Fix:** Only append cb_easp for tools marked endsAgentStep or relax schema to ignore unknown fields on autocomplete. -- **Issue:** Didn’t ensure custom tool defs are transported to backend prompts. strings.ts expects customTools but assembler not updated. **Fix:** Plumb customTools through fileContext->assembleLocalAgentTemplates->AgentTemplate so prompts receive full definitions. - **Issue:** Types in common/src/tools/list still restrict CodebuffToolCall to ToolName; executeToolCall changed to string. @@ -243,9 +251,11 @@ Add end-to-end support for user-defined custom tools alongside the built-in tool ## 2025-10-21T02:35:01.856Z — add-reasoning-options (fa43720) ### Original Agent Prompt + Add a template-level reasoning configuration that agents can specify and have it applied at runtime. Introduce an optional "reasoningOptions" field on agent definitions and dynamic templates (supporting either a max token budget or an effort level, with optional enable/exclude flags). Validate this field in the dynamic template schema. Update the streaming path so these options are passed to the OpenRouter provider as reasoning settings for each agent. Centralize any provider-specific options in the template-aware streaming code and remove such configuration from the lower-level AI SDK wrapper. Provide a baseline agent example that opts into high reasoning effort. ### Lessons + - **Issue:** Enabled reasoning in factory/base.ts, affecting all base-derived agents, instead of providing a single baseline example. **Fix:** Add reasoningOptions only in .agents/base-lite.ts to demo high-effort; keep factory defaults unchanged. @@ -276,9 +286,11 @@ Add a template-level reasoning configuration that agents can specify and have it ## 2025-10-21T02:41:42.557Z — autodetect-knowledge (00e8860) ### Original Agent Prompt + Add automatic discovery of knowledge files in the SDK run state builder. When users call the SDK without providing knowledge files but do provide project files, detect knowledge files from the provided project files and include them in the session. Treat files as knowledge files when their path ends with knowledge.md or claude.md (case-insensitive). Leave explicit knowledgeFiles untouched when provided. Update the changelog for the current SDK version to mention this behavior change. ### Lessons + - **Issue:** Used an inline IIFE in sdk/src/run-state.ts to compute fallback knowledgeFiles, hurting readability. **Fix:** Build fallback in a small helper (e.g., detectKnowledgeFilesFromProjectFiles) or a simple block; avoid IIFEs. @@ -294,16 +306,17 @@ Add automatic discovery of knowledge files in the SDK run state builder. When us ## 2025-10-21T02:41:48.918Z — update-tool-gen (f8fe9fe) ### Original Agent Prompt + Update the tool type generator to write its output into the initial agents template types file and make the web search depth parameter optional. Ensure the generator creates any missing directories so it doesn’t fail on fresh clones. Keep formatting via Prettier and adjust logs accordingly. Confirm that the agent templates continue to import from the updated tools.ts file and that no code depends on the old tools.d.ts path. Depth should be optional and default to standard behavior where omitted. ### Lessons + - **Issue:** Edited .agents/types/tools.ts unnecessarily. This is user-scaffolded output, not the generator target. **Fix:** Only write to common/src/templates/initial-agents-dir/types/tools.ts via the generator; don’t touch .agents/ files. - **Issue:** Didn’t fully verify consumers of old path common/src/util/types/tools.d.ts beyond the generator script. **Fix:** Search repo-wide (incl. non-TS files) for tools.d.ts and update imports/docs; then run a typecheck/build to confirm. -- **Issue:** Made depth optional but didn’t normalize at use sites (backend/src/tools/handlers/tool/web-search.ts). **Fix:** Default at usage: const d = depth ?? 'standard'; pass { depth: d } to searchWeb and use d for credit calc/logging. - **Issue:** Used ripgrep -t flags for unrecognized types (e.g., mjs/tsx), risking missed matches during verification. @@ -315,9 +328,9 @@ Update the tool type generator to write its output into the initial agents templ ## 2025-10-21T02:42:27.076Z — enforce-agent-auth (27d87d7) ### Original Agent Prompt -Secure the agent name validation flow and improve UX. Require an API key for the backend agent validation endpoint, return the agent display name when a match is found (both built-in and published), and have the CLI print the selected agent name immediately after successful validation. Remove the early startup agent name print to avoid duplicate/racing messages. Update tests to cover the new auth requirement and the displayName in responses. ### Lessons + - **Issue:** Used API_KEY_ENV_VAR in npm-app/src/index.ts without importing it, causing a compile/runtime error. **Fix:** Import API_KEY_ENV_VAR from @codebuff/common/constants at the top of index.ts before referencing it. @@ -330,7 +343,6 @@ Secure the agent name validation flow and improve UX. Require an API key for the - **Issue:** Agent name printing used plain 'Using agent:' without colors/format; inconsistent with CLI style. **Fix:** Print with project style: console.log(green(`\nAgent: ${bold(displayName)}`)) for consistency and readability. -- **Issue:** Backend tests assert 401 and {error} for missing API key, diverging from intended contract. **Fix:** Update tests to expect 403 and {valid:false,message:'API key required'} and keep displayName checks for success. - **Issue:** validateAgent returns void; misses chance to return displayName for downstream use/tests. @@ -345,9 +357,11 @@ Secure the agent name validation flow and improve UX. Require an API key for the ## 2025-10-21T02:44:14.254Z — fix-agent-steps (fe667af) ### Original Agent Prompt + Unify the default for the agent step limit and fix SDK behavior so that the configured maxAgentSteps reliably applies each run. Add a shared constant for the default in the config schema, make the SDK use that constant as the default run() parameter, and ensure the SDK sets stepsRemaining on the session state based on the provided or defaulted value. Update the changelog to reflect the fix. ### Lessons + - **Issue:** Config schema imported MAX_AGENT_STEPS_DEFAULT (25) from constants/agents.ts, changing default from 12 and adding cross-module coupling. **Fix:** Define DEFAULT_MAX_AGENT_STEPS=12 in common/src/json-config/constants.ts and use it in the zod .default(); treat it as the shared source. @@ -369,9 +383,9 @@ Unify the default for the agent step limit and fix SDK behavior so that the conf ## 2025-10-21T02:46:25.999Z — type-client-tools (af3f741) ### Original Agent Prompt -Strengthen and centralize typing for tool calls across the monorepo. Move the tool call types to the shared common package, define a discriminated union for client-invokable tools, and update the backend to consume these shared types. Remove the backend-local duplicates, ensure the main prompt API no longer exposes toolCalls, and align the eval scaffolding code with the new types. Keep runtime behavior unchanged—this is a typing and import refactor focused on safety and clarity. ### Lessons + - **Issue:** Added common/src/types/tools.ts duplicating schemas; lost Zod-backed runtime validation and created a second source of truth. **Fix:** Co-locate shared types with llmToolCallSchema in common/src/tools/list.ts and re-export; keep Zod-backed validation. @@ -382,7 +396,6 @@ Strengthen and centralize typing for tool calls across the monorepo. Move the to **Fix:** Narrow requestClientToolCall to ClientInvokableToolName and update all handlers to pass precise union members. - **Issue:** Handlers/stream-parser/tool-executor still rely on local types; partial migration weakens type safety. - **Fix:** Import CodebuffToolCall/ClientToolCall from common everywhere and delete backend-local type exports. - **Issue:** Changed loop-main-prompt to a single call, altering runtime behavior against the refactor-only requirement. **Fix:** Preserve loop semantics; only remove toolCalls from types/returns. If unused, delete file without logic changes. @@ -393,7 +406,6 @@ Strengthen and centralize typing for tool calls across the monorepo. Move the to - **Issue:** Evals scaffolding updated imports only; logic ignores client-invokable subset and special input shapes. **Fix:** Type toolCalls as ClientToolCall, restrict to client tools, and adapt FileChange and run_terminal_command modes. -- **Issue:** websocket requestToolCall path not constrained to client tools; accepts arbitrary tool names/params. **Fix:** Type requestToolCall and all callers to ClientInvokableToolName with params inferred from schema. - **Issue:** tool-executor/parseRawToolCall kept local types; not wired to shared unions or client-call constraints. @@ -405,9 +417,9 @@ Strengthen and centralize typing for tool calls across the monorepo. Move the to ## 2025-10-21T02:48:00.593Z — unify-api-auth (12511ca) ### Original Agent Prompt -Unify HTTP authentication between the CLI and backend by standardizing on a single API key header. Introduce small utilities to construct this header on the CLI and to extract it on the server, then update the agent validation and repository coverage endpoints, as well as the admin middleware, to use it. Keep existing response shapes and behaviors intact and ensure tests still pass. ### Lessons + - **Issue:** Used header name 'X-Codebuff-API-Key' vs canonical 'x-codebuff-api-key', causing inconsistency across CLI/server and tests. **Fix:** Standardize on 'x-codebuff-api-key' everywhere. Define a single constant and use it for both creation and extraction. @@ -418,9 +430,7 @@ Unify HTTP authentication between the CLI and backend by standardizing on a sing **Fix:** Only accept x-codebuff-api-key on HTTP endpoints. Remove Bearer fallback from server extractor used by routes. - **Issue:** Placed extractor in common/src, increasing cross-package coupling; task called for a small server utility. - **Fix:** Create a backend-local helper (e.g., backend/src/util/auth-helpers.ts) and use it in API routes/middleware. -- **Issue:** Modified backend/src/api/usage.ts, which was not within the requested endpoints, adding scope risk. **Fix:** Limit changes to the specified areas (agent validation, repo coverage, admin middleware) to reduce regression risk. - **Issue:** Logging used info-level for auth header presence in validate-agent handler, adding noise to logs. @@ -432,26 +442,20 @@ Unify HTTP authentication between the CLI and backend by standardizing on a sing ## 2025-10-21T02:48:14.602Z — add-agent-validation (26066c2) ### Original Agent Prompt + Add a lightweight agent validation system that prevents running with unknown agent IDs. On the server, expose a GET endpoint to validate an agent identifier. It should accept a required agentId query parameter, respond with whether it's valid, and include a short-lived cache for positive results. A valid agent can be either a built-in agent or a published agent, and the response should clarify which source it came from and return a normalized identifier. Handle invalid input with a 400 status and structured error. Log when authentication info is present. -On the CLI, when a specific agent is provided, validate it before starting the session. If the agent is already loaded locally, skip remote validation. Otherwise, call the backend endpoint, include any available credentials, show a spinner while checking, and exit early with a helpful message when the agent is unknown. If there is a network problem, warn and continue. Add minimal tests to cover pass-through and short-circuit cases. - ### Lessons -- **Issue:** Used getBuiltInAgents (not in repo) in backend/npm utils/tests; will not compile. - **Fix:** Use AGENT_PERSONAS/AGENT_IDS from common/src/constants/agents to detect built-ins by ID. + +**Fix:** Use AGENT_PERSONAS/AGENT_IDS from common/src/constants/agents to detect built-ins by ID. - **Issue:** Client only sent Authorization; ignored API key env. Missed 'include any credentials'. - **Fix:** Attach both Authorization (authToken) and X-API-Key from API_KEY_ENV_VAR when calling backend. - **Issue:** Server logs only noted Authorization presence; didn’t log X-API-Key as requested. **Fix:** In handler, log hasAuthHeader and hasApiKey (no secrets) alongside agentId for auditability. -- **Issue:** No backend tests added for the new GET endpoint; regressions unguarded. - **Fix:** Add tests under backend/src/api/__tests__ covering 400, builtin, published, unknown, cache hit. - -- **Issue:** CLI tests didn’t verify agentId pass-through in query string to backend. **Fix:** Add a test asserting URLSearchParams agentId equals the original (publisher/name@version). - **Issue:** Redundant loadLocalAgents call before session; duplicates earlier startup loading. @@ -472,13 +476,12 @@ On the CLI, when a specific agent is provided, validate it before starting the s ## 2025-10-21T02:48:36.995Z — refactor-agent-validation (90f0246) ### Original Agent Prompt -Refactor the CLI agent validation so that the agent name resolution happens in the CLI module rather than the main index entrypoint. Move the agent validation function into the CLI code, have it return the resolved display name without printing, and adjust the CLI startup to display the resolved agent name before the greeting. Remove the old validation function and its usage from the entry file, clean up unused imports, and update the corresponding unit test to import from the new location. Keep the existing backend endpoint contract intact. ### Lessons + - **Issue:** CLI.validateAgent returns undefined for local agents, so the caller can’t print the resolved name. **Fix:** On local hit, return the displayName (id->config or name match), e.g., localById?.displayName || localByDisplay?.displayName || agent. -- **Issue:** printInitialPrompt uses loadedAgents without ensuring they’re loaded, risking race/unnecessary backend calls. **Fix:** await loadLocalAgents({verbose:false}) before validateAgent; pass agents into it, then print name, then displayGreeting. - **Issue:** validateAgent defaults to getCachedLocalAgentInfo which may be empty/stale, breaking local resolution. @@ -493,9 +496,11 @@ Refactor the CLI agent validation so that the agent name resolution happens in t ## 2025-10-21T02:51:02.634Z — add-run-state-helpers (6a107de) ### Original Agent Prompt + Add new run state helper utilities to the SDK to make it easy to create and modify runs, and refactor the client and exports to use them. Specifically: introduce a module that can initialize a fresh SessionState and wrap it in a RunState, provide helpers to append a new message or replace the entire message history for continuing a run, update the client to use this initializer instead of its local implementation, and expose these helpers from the SDK entrypoint. Update the README to show a simple example where a previous run is augmented with an image message before continuing, and bump the SDK version and changelog accordingly. ### Lessons + - **Issue:** Helper names diverged from expected API (used create*/make*/append*/replace* vs initialSessionState/generate*/withAdditional*/withMessageHistory). **Fix:** Match the intended names: initialSessionState, generateInitialRunState, withAdditionalMessage, withMessageHistory; update client/README accordingly. @@ -517,9 +522,11 @@ Add new run state helper utilities to the SDK to make it easy to create and modi ## 2025-10-21T02:52:33.654Z — fix-agent-publish (4018082) ### Original Agent Prompt + Update the agent publishing pipeline so the publish API accepts raw agent definitions, validates them centrally, and allows missing prompts. On the validator side, return both compiled agent templates and their validated dynamic forms. In the CLI, adjust agent selection by id/displayName and send raw definitions to the API. Ensure that optional prompts are treated as empty strings during validation and that the API responds with clear validation errors when definitions are invalid. ### Lessons + - **Issue:** Publish request schema still enforces DynamicAgentDefinitionSchema[] (common/src/types/api/agents/publish.ts), rejecting truly raw defs. **Fix:** Accept fully raw input: data: z.record(z.string(), z.any()).array(). Validate centrally via validateAgents in the API route. @@ -535,31 +542,22 @@ Update the agent publishing pipeline so the publish API accepts raw agent defini ## 2025-10-21T02:56:18.897Z — centralize-placeholders (29d8f3f) ### Original Agent Prompt -Unify agent prompt placeholders by centralizing PLACEHOLDER and its types in the secret agent definitions and updating all agent prompt/factory modules to import from there. Remove the old backend prompt files that duplicated this logic. Make sure there are no dangling references and that prompt formatting still injects the same values at runtime. ### Lessons + - **Issue:** Imported PLACEHOLDER from a non-existent path (@codebuff/common/.../secret-agent-definition), causing dangling refs. **Fix:** Only import from existing modules or add the file first. Create the common secret-agent-definition.ts before updating imports. - **Issue:** Changed common/agent-definition.ts to re-export from './secret-agent-definition' which doesn’t exist in common. **Fix:** Either add common/.../secret-agent-definition.ts or re-export from an existing module. Don’t point to files that aren’t there. -- **Issue:** Left duplicated backend prompt files (backend/src/templates/{ask-prompts,base-prompts}.ts) instead of removing them. - **Fix:** Delete the old backend prompt files per spec. Ensure all references point to the unified agents prompts; no duplicate logic remains. - -- **Issue:** Modified backend prompt files that were meant to be removed, increasing diff noise and risk of drift. **Fix:** Avoid editing files scheduled for deletion. Remove them and update imports/usage sites to the single source of truth. - **Issue:** Centralized across packages without a clear plan, introducing cross-package breakage and unresolved imports. - **Fix:** Pick one canonical location (common). Add the file there, then re-export via backend/src/templates/types.ts to keep consumers stable. - -- **Issue:** Did not ensure all backend consumers import via a single re-export point; mixed direct and central imports. - **Fix:** Make backend/src/templates/types.ts the sole backend import point. Update backend files to import PLACEHOLDER/typing from './types'. - **Issue:** Did not validate the repo after refactor (no typecheck/build), so broken imports slipped in. **Fix:** Run a full typecheck/build after edits. Fix any unresolved modules before concluding to meet the “no dangling refs” requirement. -- **Issue:** Changed import paths in backend/strings.ts to a path that wasn’t created, risking runtime failures. **Fix:** Update strings.ts only after the target module exists. If centralizing, add the module first, then adjust imports. - **Issue:** Did not verify that prompt formatting still injects the same values at runtime post-refactor. @@ -571,9 +569,11 @@ Unify agent prompt placeholders by centralizing PLACEHOLDER and its types in the ## 2025-10-21T02:58:10.976Z — add-sdk-terminal (660fa34) ### Original Agent Prompt + Add first-class SDK support for running terminal commands via the run_terminal_command tool. Implement a synchronous, cross-platform shell execution helper with timeout and project-root cwd handling, and wire it into the SDK client’s tool-call flow. Ensure the tool-call-response uses the standardized output object instead of the previous result string and that errors are surfaced as text output. Match the behavior and message schema used by the server and the npm app, but keep the SDK implementation minimal without background mode. ### Lessons + - **Issue:** Used spawnSync, blocking Node’s event loop during command runs; hurts responsiveness even for short commands. **Fix:** Use spawn with a Promise and a kill-on-timeout guard. Keep SYNC semantics at tool level without blocking the event loop. @@ -586,18 +586,15 @@ Add first-class SDK support for running terminal commands via the run_terminal_c - **Issue:** When returning a terminal_command_error payload, success stayed true and error field was empty. **Fix:** If output contains a terminal_command_error, also populate error (and optionally set success=false) for clearer signaling. -- **Issue:** handleToolCall lacked an explicit return type tied to WebSocketHandler, risking drift from schema. - **Fix:** Annotate return type as ReturnType to lock to the expected schema. - - **Issue:** Timeout/termination status omitted the signal, reducing diagnostic clarity on killed processes. **Fix:** Include res.signal (e.g., 'Terminated by signal: SIGTERM') in status when present to improve parity and debuggability. ## 2025-10-21T02:59:05.311Z — align-agent-types (ea45eda) ### Original Agent Prompt -Unify the .agents local agent typing and examples with the repository’s established tool call and schema shapes. Ensure all tool calls use an input object (not args), and require JsonObjectSchema for input/output object schemas. Align the documentation comments and the three example agents under .agents/examples with these conventions without changing backend or common packages. ### Lessons + - **Issue:** Example 01 used find_files with input.prompt; param name likely mismatched the tool schema, risking runtime/type errors. **Fix:** Check .agents/types/tools.ts and use the exact params find_files expects (e.g., correct key names) inside input. @@ -622,9 +619,11 @@ Unify the .agents local agent typing and examples with the repository’s establ ## 2025-10-21T03:00:16.042Z — surface-history-access (6bec422) ### Original Agent Prompt + Make dynamic agents not inherit prior conversation history by default. Update the generated spawnable agents description so that, for any agent that can see the current message history, the listing explicitly states that capability. Keep showing each agent’s input schema (prompt and params) when available, otherwise show that there is none. Ensure the instructions prompt includes tool instructions, the spawnable agents description, and output schema details where applicable. ### Lessons + - **Issue:** Added extra visibility lines (negative/unknown) in spawnable agents description beyond spec. **Fix:** Only append "This agent can see the current message history." when includeMessageHistory is true; omit else/unknown lines. @@ -637,13 +636,15 @@ Make dynamic agents not inherit prior conversation history by default. Update th ## 2025-10-21T03:04:04.761Z — move-agent-templates (26e84af) ### Original Agent Prompt + Centralize the built-in agent templates and type definitions under a new common/src/templates/initial-agents-dir. Update the CLI to scaffold user .agents files by copying from this new location instead of bundling from .agents. Update all imports in the SDK and common to reference the new AgentDefinition/ToolCall types path. Remove the old re-export that pointed to .agents so consumers can’t import from the legacy location. Keep runtime loading of user-defined agents from .agents unchanged and ensure the codebase builds cleanly. ### Lessons + - **Issue:** Kept common/src/types/agent-definition.ts as a re-export (now to new path) instead of removing it, weakening path enforcement. **Fix:** Delete the file or stop re-exporting. Force consumers to import from common/src/templates/.../agent-definition directly. -- **Issue:** Missed updating test import in common/src/types/__tests__/dynamic-agent-template.test.ts to the new AgentDefinition path. +- **Issue:** Missed updating test import in common/src/types/**tests**/dynamic-agent-template.test.ts to the new AgentDefinition path. **Fix:** Change import to '../../templates/initial-agents-dir/types/agent-definition' so type-compat tests build and validate correctly. - **Issue:** Introduced types/secret-agent-definition.ts under initial-agents-dir, which wasn’t requested and adds scope creep. @@ -661,9 +662,11 @@ Centralize the built-in agent templates and type definitions under a new common/ ## 2025-10-21T03:04:54.094Z — add-agent-resolution (de3ea46) ### Original Agent Prompt + Add agent ID resolution and improve the CLI UX for traces, agents listing, and publishing. Specifically: create a small utility that resolves a CLI-provided agent identifier by preserving explicit org prefixes, leaving known local IDs intact, and defaulting unknown unprefixed IDs to a default org prefix. Use this resolver in both the CLI and client when showing the selected agent and when sending requests. Replace usage of the old subagent trace viewer with a new traces handler that improves the status hints and allows pressing 'q' to go back (in both the trace buffer and the trace list). Update the agents menu to group valid custom agents by last modified time, with a "Recently Updated" section for the past week and a "Custom Agents" section for the rest; show a placeholder when none exist. Finally, make publishing errors clearer by printing a concise failure line, optional details, and an optional hint, and ensure the returned error contains non-duplicated fields for callers. Keep the implementation consistent with existing patterns in the codebase. ### Lessons + - **Issue:** Kept using cli-handlers/subagent.ts; no new traces handler or import updates in cli.ts/client.ts/subagent-list.ts. **Fix:** Create cli-handlers/traces.ts, move trace UI there, and update all imports to './traces' with improved status and 'q' support. @@ -685,9 +688,11 @@ Add agent ID resolution and improve the CLI UX for traces, agents listing, and p ## 2025-10-21T03:10:54.539Z — add-prompt-error (9847358) ### Original Agent Prompt + Introduce a distinct error channel for user prompts. Add a new server action that specifically reports prompt-related failures, wire server middleware and the main prompt execution path to use it when the originating request is a prompt, and update the CLI client to listen for and display these prompt errors just like general action errors. Keep existing success and streaming behaviors unchanged. ### Lessons + - **Issue:** Defined prompt-error with promptId; codebase standardizes on userInputId (e.g., response-chunk). Inconsistent ID naming. **Fix:** Use userInputId in prompt-error schema/payload and pass action.promptId into it. Keep ID fields consistent across actions. @@ -709,9 +714,11 @@ Introduce a distinct error channel for user prompts. Add a new server action tha ## 2025-10-21T03:12:06.098Z — stop-think-deeply (97178a8) ### Original Agent Prompt + Update the agent step termination so that purely reflective planning tools do not cause another step. Introduce a shared list of non-progress tools (starting with think_deeply) and adjust the end-of-step logic to end the turn whenever only those tools were used, while still ending on explicit end_turn. Keep the change minimal and localized to the agent step logic and shared tool constants. ### Lessons + - **Issue:** Termination checked only toolCalls; toolResults were ignored. If a result from a progress tool appears, the step might not end correctly. **Fix:** Filter both toolCalls and toolResults by non-progress list; end when no progress items remain in either array (mirrors ground-truth logic). @@ -730,9 +737,11 @@ Update the agent step termination so that purely reflective planning tools do no ## 2025-10-21T03:13:08.010Z — update-agent-builder (ab4819b) ### Original Agent Prompt + Update the agent builder and example agents to support a new starter custom agent and align example configurations. Specifically: make the agent builder gather both existing diff-reviewer examples and a new your-custom-agent starter template; copy the starter template directly into the top-level agents directory while keeping examples under the examples subfolder; remove advertised spawnable agents from the builder; fix the agent personas to remove an obsolete entry and correct a wording typo; and refresh the diff-reviewer examples to use the current Anthropic model, correct the file-explorer spawn target, and streamline the final step behavior. Also add a new your-custom-agent file that scaffolds a Git Committer agent ready to run and publish. ### Lessons + - **Issue:** Removed wrong persona in common/src/constants/agents.ts (deleted claude4_gemini_thinking, left base_agent_builder). **Fix:** Remove base_agent_builder entry and keep others. Also fix typo to 'multi-agent' in agent_builder purpose. @@ -748,28 +757,29 @@ Update the agent builder and example agents to support a new starter custom agen - **Issue:** Builder injected publisher/version into starter via brittle string replaces and './constants' import. **Fix:** Author the starter file ready-to-use; builder should copy as-is to .agents root without string mutation/injection. -- **Issue:** Updated .agents/examples/* directly (generated outputs), causing duplication and drift. +- **Issue:** Updated .agents/examples/\* directly (generated outputs), causing duplication and drift. **Fix:** Only update source examples under common/src/util/examples; let the builder copy them to .agents/examples. - **Issue:** diff-reviewer-3 example text wasn’t aligned with streamlined flow (kept separate review message step). **Fix:** Merge intent into step 4 message (spawn explorer then review) and end with a single 'yield STEP_ALL'. -- **Issue:** Left unused symbols (e.g., DEFAULT_MODEL) in backend/src/templates/agents/agent-builder.ts. **Fix:** Remove or use unused constants/imports to avoid noUnusedLocals warnings after refactors. ## 2025-10-21T03:13:39.771Z — overhaul-agent-examples (bf5872d) ### Original Agent Prompt -Overhaul the example agents and CLI scaffolding. Replace the older diff-reviewer-* examples with three new examples (basic diff reviewer, intermediate git committer, advanced file explorer), update the CLI to create these files in .agents/examples, enhance the changes-reviewer agent to be able to spawn the file explorer while reviewing diffs or staged changes, add structured output to the file-explorer agent, and revise the default my-custom-agent to focus on reviewing changes rather than committing. Keep existing types and README generation intact. + +Overhaul the example agents and CLI scaffolding. Replace the older diff-reviewer-\* examples with three new examples (basic diff reviewer, intermediate git committer, advanced file explorer), update the CLI to create these files in .agents/examples, enhance the changes-reviewer agent to be able to spawn the file explorer while reviewing diffs or staged changes, add structured output to the file-explorer agent, and revise the default my-custom-agent to focus on reviewing changes rather than committing. Keep existing types and README generation intact. ### Lessons + - **Issue:** changes-reviewer spawnPurposePrompt didn’t mention staged changes. **Fix:** Update spawnPurposePrompt to “review code in git diff or staged changes” in .agents/changes-reviewer.ts. - **Issue:** changes-reviewer didn’t guide spawning the file explorer during review. **Fix:** Inject an add_message hint before STEP_ALL to prompt spawning file-explorer and add spawn_agents usage. -- **Issue:** Old .agents/examples/diff-reviewer-*.ts files were left in repo. +- **Issue:** Old .agents/examples/diff-reviewer-\*.ts files were left in repo. **Fix:** Delete diff-reviewer-1/2/3.ts to fully replace them with the new examples and avoid confusion. - **Issue:** Advanced example agent lacks an outputSchema while using structured_output. @@ -790,9 +800,11 @@ Overhaul the example agents and CLI scaffolding. Replace the older diff-reviewer ## 2025-10-21T03:14:43.174Z — update-validation-api (0acdecd) ### Original Agent Prompt + Simplify the agent validation flow to not require authentication and to use an array-based payload. Update the CLI helper to send an array of local agent configs and call the web validation API without any auth. Update the web validation endpoint to accept an array, convert it to the format expected by the shared validator, and return the same response structure. Make sure initialization validates local agents even when the user is not logged in, and keep logging and error responses clear. ### Lessons + - **Issue:** Changed validate API payload to a top-level array, breaking callers expecting { agentConfigs }. See utils/agent-validation.ts and web route. **Fix:** Keep request envelope { agentConfigs: [...] } in client and server; convert to record internally; remove auth only. @@ -808,9 +820,9 @@ Simplify the agent validation flow to not require authentication and to use an a ## 2025-10-21T03:17:32.159Z — migrate-agents (02ef7c0) ### Original Agent Prompt -Migrate custom agent scaffolding to a first-class .agents directory and shift file generation to the CLI. Add TypeScript type modules for agent definitions and tools under .agents/types, include a starter agent and three example diff reviewers, and provide a concise README for users. Update the backend agent builder to be model-only (no file I/O) and embed the type content for reference in its instructions. Remove legacy type/example copies in common, fix imports across common and sdk to point at the canonical types exported by common/src/types, and adjust the CLI to create the .agents directories/files using bundled text imports. Ensure the example agents use the modern model and spawnable agent IDs, and streamline their step flow. ### Lessons + - **Issue:** Did not add .agents/types modules; used inline .d.ts strings from CLI scaffolding. **Fix:** Create .agents/types/agent-definition.ts and tools.ts files and bundle them; import as text where needed. @@ -845,7 +857,7 @@ Migrate custom agent scaffolding to a first-class .agents directory and shift fi **Fix:** Avoid a top-level types.ts; add common/src/types/agent-definition.ts and re-export canonical .agents types. - **Issue:** SDK build scripts still copy legacy util/types; risk breakage after deletion. - **Fix:** Remove copy-types step in sdk/package.json; have sdk/src/types/* re-export from @codebuff/common/types. + **Fix:** Remove copy-types step in sdk/package.json; have sdk/src/types/\* re-export from @codebuff/common/types. - **Issue:** Imports across common/sdk not fully updated to canonical common/src/types. **Fix:** Point all imports (including tests) to '@codebuff/common/types' or local common/src/types re-exports. @@ -856,14 +868,14 @@ Migrate custom agent scaffolding to a first-class .agents directory and shift fi ## 2025-10-21T03:18:26.438Z — restore-subagents-field (b30e2ef) ### Original Agent Prompt + Migrate the AgentState structure to use a 'subagents' array instead of 'spawnableAgents' across the schema, state initialization, spawn handlers, and tests. Ensure all places that construct or validate AgentState use 'subagents' consistently while leaving AgentTemplate.spawnableAgents intact. Update developer-facing JSDoc to clarify how to specify spawnable agent IDs. Keep the existing agent spawning behavior unchanged. ### Lessons + - **Issue:** Missed migrating async spawn handler: spawn-agents-async.ts still sets AgentState.spawnableAgents: []. - **Fix:** Update backend/src/tools/handlers/tool/spawn-agents-async.ts to set subagents: [] when constructing child AgentState. - **Issue:** Tests not updated: sandbox-generator.test.ts still builds AgentState with spawnableAgents: []. - **Fix:** Change mock AgentState in backend/src/__tests__/sandbox-generator.test.ts to use subagents: [] to match AgentStateSchema. - **Issue:** JSDoc for spawnable agent IDs is vague; doesn’t mandate fully-qualified IDs with publisher and version. **Fix:** Update docs to require 'publisher/name@version' or local '.agents' id. Mirror this in common/src/util/types/agent-config.d.ts. @@ -877,9 +889,11 @@ Migrate the AgentState structure to use a 'subagents' array instead of 'spawnabl ## 2025-10-21T03:23:52.779Z — expand-agent-types (68e4f6c) ### Original Agent Prompt + We need to let our internal .agents declare a superset of tools (including some client-only/internal tools) without affecting public agent validation. Add a new SecretAgentDefinition type for .agents that accepts these internal tools, switch our built-in agents to use it, and keep dynamic/public agents constrained to the public tool list. Also relocate the publishedTools constant from the tools list module to the tools constants module and update any imports that depend on it. No runtime behavior should change—this is a type/constant refactor that must compile cleanly and keep existing tests green. ### Lessons + - **Issue:** Did not add a dedicated SecretAgentDefinition for .agents to allow internal tools. **Fix:** Create .agents/types/secret-agent-definition.ts extending AgentDefinition with toolNames?: AllToolNames[]. @@ -905,7 +919,7 @@ We need to let our internal .agents declare a superset of tools (including some **Fix:** Make a type/constant-only refactor; do not change llmToolCallSchema, handlers, or runtime code paths. - **Issue:** Missed updating all agent files to the new type (some remained on AgentDefinition). - **Fix:** Grep all .agents/*.ts and replace AgentDefinition with SecretAgentDefinition consistently (incl. oss agents). + **Fix:** Grep all .agents/\*.ts and replace AgentDefinition with SecretAgentDefinition consistently (incl. oss agents). - **Issue:** Didn’t validate the refactor with a compile/test pass. **Fix:** Run typecheck/tests locally to catch missing imports or schema mismatches and keep tests green. @@ -913,9 +927,9 @@ We need to let our internal .agents declare a superset of tools (including some ## 2025-10-21T03:26:22.005Z — migrate-agent-validation (2b5651f) ### Original Agent Prompt -Move dynamic agent validation out of the WebSocket init path and into a dedicated authenticated web API, and have the CLI validate locally loaded agents through that API when a user is logged in. Introduce a small CLI utility to call the API and print any validation warnings. Update the project file context to load local agent configs directly at initialization and avoid mixing agent templates into knowledge files. Finally, simplify the server init response to just usage data so the CLI no longer expects WebSocket-delivered agent names or validation messages. ### Lessons + - **Issue:** API route expects 'agents' but CLI util posts 'agentConfigs' (utils/agent-validation.ts) → 400s get swallowed. **Fix:** Standardize payload to 'agentConfigs' across route and callers; validate and return clear errors. @@ -926,7 +940,6 @@ Move dynamic agent validation out of the WebSocket init path and into a dedicate **Fix:** Send Cookie: next-auth.session-token (like other CLI calls); drop authToken from body. - **Issue:** dynamic-agents.knowledge.md was not removed; stale doc risks being ingested as knowledge. - **Fix:** Delete backend/src/templates/dynamic-agents.knowledge.md to avoid mixing templates into knowledge. - **Issue:** ProjectFileContext still sources agentTemplates from global loadedAgents (implicit state). **Fix:** Assign agentTemplates from await loadLocalAgents(...) return; avoid globals to prevent staleness. @@ -940,11 +953,10 @@ Move dynamic agent validation out of the WebSocket init path and into a dedicate ## 2025-10-21T03:30:33.249Z — relocate-ws-errors (70239cb) ### Original Agent Prompt -Move WebSocket action send error handling out of the shared library and into the CLI app. The shared WebSocket client should no longer terminate the process on send failures; it should just propagate errors. In the CLI, add a small wrapper around action sends that logs a concise error, prints a helpful update message telling the user to update to the latest version, and exits. Replace the direct action send calls in the CLI with this wrapper so all action sends are covered. Leave the SDK and backend untouched. ### Lessons + - **Issue:** Wrapper sendActionOrExit initially called itself, causing infinite recursion and potential stack overflow. - **Fix:** Call this.webSocket.sendAction(action) inside the wrapper; run a quick manual/test call to catch recursion bugs early. - **Issue:** Wrapper returned Promise|void with a thenable check, making behavior/contract unclear and harder to reason about. **Fix:** Implement wrapper as async and always await sendAction; explicitly return Promise and catch/exit on errors. @@ -953,19 +965,19 @@ Move WebSocket action send error handling out of the shared library and into the **Fix:** Stop the spinner before exiting: Spinner.get().stop(); then log the error, print update guidance, and process.exit(1). - **Issue:** No explicit verification that all CLI sendAction call sites were wrapped (only client.ts was updated). - **Fix:** Run a CLI-wide search for sendAction/webSocket.sendAction and replace all with the wrapper to ensure total coverage. - **Issue:** If socket isn’t OPEN, sendAction returns undefined; wrapper gives no feedback, so failed sends silently noop. - **Fix:** Detect non-Promise return and log a warning (e.g., logger.warn) that the WebSocket isn’t open and the action wasn’t sent. ## 2025-10-21T03:34:04.751Z — bundle-agent-types (5484add) ### Original Agent Prompt + Internalize the AgentConfig definition and related tool type definitions within the SDK so that consumers import types directly from @codebuff/sdk. Update the SDK build to copy the .d.ts type sources from the monorepo’s common package into the SDK before compiling, adjust the client to import AgentConfig from the SDK’s local types, and update the SDK entrypoint to re-export AgentConfig as a type. Add the corresponding type files under sdk/src/util/types to mirror the common definitions and keep them self-contained. ### Lessons + - **Issue:** Types weren’t copied from common to SDK before compile; a post-build copy was added from src→dist instead. - **Fix:** Add a prebuild step to copy ../common/src/util/types/*.d.ts into sdk/src/util/types before tsc runs. + **Fix:** Add a prebuild step to copy ../common/src/util/types/\*.d.ts into sdk/src/util/types before tsc runs. - **Issue:** Build order was wrong: ran tsc then copied .d.ts, so they weren’t part of the compilation pipeline. **Fix:** Invoke copy first, then compile (e.g., "bun run copy-types && tsc") so types are available during build. @@ -983,7 +995,7 @@ Internalize the AgentConfig definition and related tool type definitions within **Fix:** Add "copy-types" script (mkdir/cp) and call it in build: "bun run copy-types && tsc". - **Issue:** Didn’t validate publish output alignment; potential mismatch of exports/types paths in dist. - **Fix:** Run npm pack --dry-run on dist, verify dist/sdk/src/util/types/*.d.ts exists and exports/types resolve. + **Fix:** Run npm pack --dry-run on dist, verify dist/sdk/src/util/types/\*.d.ts exists and exports/types resolve. - **Issue:** Introduced unrelated changes (bun.lock, extra deps) not required for the task. **Fix:** Limit diffs to required files; avoid lockfile/dependency churn unless necessary for the feature. @@ -991,9 +1003,9 @@ Internalize the AgentConfig definition and related tool type definitions within ## 2025-10-21T03:34:42.036Z — fork-read-files (349a140) ### Original Agent Prompt -Decouple the SDK’s file reading from the npm app. Add an internal SDK helper that reads files relative to the client’s working directory, enforces a reasonable size limit, and returns standardized status markers for missing, too-large, out-of-bounds, or error cases. Update the SDK client to use this helper and pass its cwd. Preserve the response shape and status values expected by the backend. Avoid introducing dependencies on the npm app. ### Lessons + - **Issue:** sdk/src/tools/read-files.ts keyed results by originalPath, risking mismatch if server sends absolute paths. **Fix:** Key results by path.relative(cwd, absolutePath) so returned keys are cwd-relative and stable regardless of input form. @@ -1012,23 +1024,25 @@ Decouple the SDK’s file reading from the npm app. Add an internal SDK helper t ## 2025-10-21T03:35:51.223Z — update-sdk-types (73a0d35) ### Original Agent Prompt + In the SDK package, move the agent/tool type definitions into a new src/types directory and update internal imports to use it. Adjust the build step that copies type declarations to target the new directory. Simplify the publishing flow so that verification and publishing occur from the sdk directory (no rewriting package.json in dist). Update the package exports to reference the built index path that aligns with publishing from the sdk directory, include the changelog in package files, bump the version, and update the changelog to document the latest release with the completed client and new run() API. ### Lessons -- **Issue:** package.json main/types/exports kept ./dist/index.*; doesn’t align with publishing from sdk or monorepo dist layout. + +- **Issue:** package.json main/types/exports kept ./dist/index.\*; doesn’t align with publishing from sdk or monorepo dist layout. **Fix:** Update main/types/exports to the actual built entry (e.g. ./dist/sdk/src/index.js/.d.ts) to match the publish cwd and build output. -- **Issue:** SDK code still imports ../../common/src/*; publishing from sdk omits common, breaking runtime resolution. +- **Issue:** SDK code still imports ../../common/src/\*; publishing from sdk omits common, breaking runtime resolution. **Fix:** Replace relative common imports with a proper package dep (e.g. @codebuff/common) or point entry to a build that includes common. -- **Issue:** Committed src/types/*.ts while still running copy-types to overwrite them, risking drift and confusing source of truth. +- **Issue:** Committed src/types/\*.ts while still running copy-types to overwrite them, risking drift and confusing source of truth. **Fix:** Pick one source: either generate at build (keep copy-types, don’t commit files) or commit types and remove the copy-types step. - **Issue:** Version bump and CHANGELOG didn’t follow existing style/timeline (0.2.0 vs expected 0.1.x; removed intro line; dates/notes off). **Fix:** Match repo’s semver and format. Bump to the intended version, keep the header line, and add notes for completed client and run() API. - **Issue:** Exports path wasn’t updated to the built index that matches simplified publish (npm pack from sdk, not dist/). - **Fix:** Ensure exports map points to built files reachable when packing from sdk (e.g. types/import/default -> ./dist/sdk/src/index.*). + **Fix:** Ensure exports map points to built files reachable when packing from sdk (e.g. types/import/default -> ./dist/sdk/src/index.\*). - **Issue:** Did not validate that removing util/types or adding src/types keeps ts outputs consistent and avoids duplicate emit. **Fix:** After moving types, remove old dir and verify tsconfig include/exclude produce a single set of .js/.d.ts without duplicates. @@ -1036,9 +1050,9 @@ In the SDK package, move the agent/tool type definitions into a new src/types di ## 2025-10-21T03:37:19.438Z — stream-event-bridge (e3c563e) ### Original Agent Prompt -Enhance the SDK client so that callers can optionally receive streamed structured events during a run. Add an optional event handler to the run API that gets called with structured streaming events, and wire the WebSocket response streaming to deliver those events for the corresponding prompt. Ensure WebSocket errors are surfaced via the provided error callback. Also fix the file change tool to import the patch utility using the correct relative path in this monorepo. ### Lessons + - **Issue:** Event handlers aren’t cleared on non-success paths (schema fail, action-error, cancel, reconnect), risking leaks in promptIdToEventHandler. **Fix:** Always delete handlers on all end paths: in onResponseError, on PromptResponseSchema reject, on reconnect/close, and when canceling a run. @@ -1051,9 +1065,9 @@ Enhance the SDK client so that callers can optionally receive streamed structure ## 2025-10-21T03:37:33.756Z — spawn-inline-agent (dac33f3) ### Original Agent Prompt -Add a new tool that lets an agent spawn a child agent inline, sharing the current conversation history and returning control after the child ends its turn. Register the tool across shared schemas and backend registries, implement the handler to run the child agent within the same message list, and ensure no separate tool result is emitted—the shared history updates are the effect. Update tests to cover inline spawning, message deletion via set_messages, and TTL-based expiration of temporary prompts. Preserve subagent permission checks and schema validation for prompt and params. ### Lessons + - **Issue:** Inline handler didn’t expire 'userPrompt' TTL after child finishes, leaving temporary prompts in history. **Fix:** After child run, call expireMessages(finalMessages, 'userPrompt') and write back to state/messages to purge temp prompts. @@ -1081,9 +1095,11 @@ Add a new tool that lets an agent spawn a child agent inline, sharing the curren ## 2025-10-21T03:37:39.469Z — support-agentconfigs (2fcbe70) ### Original Agent Prompt + Enhance the SDK to accept multiple custom agents in a single run and provide a reusable AgentConfig type. Introduce a shared type module that defines both AgentConfig (for user-supplied agent definitions) and ToolCall, export AgentConfig from the SDK entrypoint, and update the SDK client API to take an agentConfigs array. When preparing session state, convert this array into the agentTemplates map, stringifying any handleSteps functions. Refresh the README to document agentConfigs with a brief example and update the parameter reference accordingly. ### Lessons + - **Issue:** Breaking API change: agentConfig -> agentConfigs without backward-compat handling. **Fix:** Accept legacy agentConfig (map) and convert to agentTemplates, while supporting new agentConfigs[]. Deprecate with warning. @@ -1105,13 +1121,14 @@ Enhance the SDK to accept multiple custom agents in a single run and provide a r ## 2025-10-21T03:38:58.318Z — unify-agent-builder (4852954) ### Original Agent Prompt + Unify the agent-builder system into a single builder, update agent type definitions to use structured output, and introduce three diff-reviewer example agents. Remove the deprecated messaging tool and update the agent registry and CLI flows to target the unified builder. Ensure the builder prepares local .agents/types and .agents/examples, copies the correct type definitions and example agents from common, and leaves agents and examples ready to compile and run. ### Lessons + - **Issue:** Unified the wrong builder: removed agent_builder and kept base_agent_builder across registry/types/personas. **Fix:** Keep agent_builder as the single builder, remove base_agent_builder and update all refs to AgentTemplateTypes.agent_builder. -- **Issue:** Registry removed agent_builder entry, leaving no unified builder registered (backend/src/templates/agent-list.ts). **Fix:** In agent-list.ts, import and register ./agents/agent-builder as AgentTemplateTypes.agent_builder; drop base_agent_builder. - **Issue:** CLI flows still target base_agent_builder (npm-app/src/cli-handlers/agent-creation-chat.ts, agents.ts). @@ -1136,7 +1153,6 @@ Unify the agent-builder system into a single builder, update agent type definiti **Fix:** Create .agents/examples/diff-reviewer-{1,2,3}.ts; ensure correct imports; builder should copy them into that folder. - **Issue:** Builder didn’t reliably prepare .agents/examples and copy correct example set from common. - **Fix:** In backend agent-builder, mkdir -p .agents/types and .agents/examples; copy diff-reviewer-* from common into examples. - **Issue:** Builder/types sync gap: updated common and sdk types but not the local .agents/types used by user agents. **Fix:** Have the builder write current common types into .agents/types (agent-config.d.ts, tools.d.ts) so locals compile. @@ -1147,9 +1163,11 @@ Unify the agent-builder system into a single builder, update agent type definiti ## 2025-10-21T03:44:28.949Z — add-agent-store (95883eb) ### Original Agent Prompt + Build a public Agent Store experience. Add a new /agents page that lists published agents with search and sorting and links into existing agent detail pages. Implement a simple /api/agents list endpoint that pulls agents from the database, joins publisher info, includes basic summary fields from the agent JSON, and adds placeholder usage metrics. Update the site navigation to include an "Agent Store" link in both the header and the user dropdown. Keep the implementation aligned with the existing agent detail route structure and the current database schema. ### Lessons + - **Issue:** Agents page used native /\n+ \n+ \n+ \n+ \n+ \n+ Most Used\n+ Newest\n+ Name\n+ Total Spent\n+ \n+ \n+ \n+ \n+ \n+ {/* Agent Grid */}\n+ {isLoading ? (\n+
\n+ {Array.from({ length: 6 }).map((_, i) => (\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+ \n+ \n+
\n+
\n+
\n+ ))}\n+
\n+ ) : (\n+ \n+ {filteredAndSortedAgents.map((agent, index) => (\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+ {agent.name}\n+ \n+
\n+ \n+ by @{agent.publisher.id}\n+ \n+ {agent.publisher.verified && (\n+ \n+ ✓\n+ \n+ )}\n+
\n+
\n+ \n+
\n+
\n+ \n+

\n+ {agent.description}\n+

{' '}\n+ {/* Usage Stats */}\n+
\n+
\n+ \n+ \n+ {formatUsageCount(agent.usage_count)}\n+ \n+ uses\n+
\n+
\n+ \n+ \n+ {formatCurrency(agent.total_spent)}\n+ \n+ spent\n+
\n+
\n+ \n+ \n+ {formatCurrency(agent.avg_cost_per_invocation)}\n+ \n+ per use\n+
\n+
\n+ \n+ v{agent.version}\n+ \n+
\n+
\n+ {/* Tags */}\n+ {agent.tags && agent.tags.length > 0 && (\n+
\n+ {agent.tags.slice(0, 3).map((tag) => (\n+ \n+ {tag}\n+ \n+ ))}\n+ {agent.tags.length > 3 && (\n+ \n+ +{agent.tags.length - 3}\n+ \n+ )}\n+
\n+ )}\n+
\n+
\n+ \n+ \n+ ))}\n+ \n+ )}\n+ {filteredAndSortedAgents.length === 0 && !isLoading && (\n+ \n+
\n+ \n+

No agents found

\n+

Try adjusting your search or filter criteria

\n+
\n+
\n+ )}\n+ \n+ \n+ )\n+}\n+\n+export default AgentStorePage\n" + "diff": "Index: web/src/app/agents/page.tsx\n===================================================================\n--- web/src/app/agents/page.tsx\t5c8c14c (parent)\n+++ web/src/app/agents/page.tsx\t95883eb (commit)\n@@ -1,1 +1,283 @@\n-[NEW FILE]\n\\ No newline at end of file\n+'use client'\n+\n+import { useState, useMemo } from 'react'\n+import { useQuery } from '@tanstack/react-query'\n+import { motion } from 'framer-motion'\n+import {\n+ Search,\n+ TrendingUp,\n+ Clock,\n+ Star,\n+ Users,\n+ ChevronRight,\n+} from 'lucide-react'\n+import Link from 'next/link'\n+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'\n+import { Badge } from '@/components/ui/badge'\n+import { Skeleton } from '@/components/ui/skeleton'\n+import { Input } from '@/components/ui/input'\n+import {\n+ Select,\n+ SelectContent,\n+ SelectItem,\n+ SelectTrigger,\n+ SelectValue,\n+} from '@/components/ui/select'\n+import { AnimatedElement } from '@/components/ui/landing/animated-element'\n+\n+interface AgentData {\n+ id: string\n+ name: string\n+ description?: string\n+ publisher: {\n+ id: string\n+ name: string\n+ verified: boolean\n+ }\n+ version: string\n+ created_at: string\n+ usage_count?: number\n+ total_spent?: number\n+ avg_cost_per_invocation?: number\n+ avg_response_time?: number\n+\n+ tags?: string[]\n+}\n+\n+const AgentStorePage = () => {\n+ const [searchQuery, setSearchQuery] = useState('')\n+ const [sortBy, setSortBy] = useState('usage')\n+\n+ // Fetch agents from the API\n+ const { data: agents = [], isLoading } = useQuery({\n+ queryKey: ['agents'],\n+ queryFn: async () => {\n+ const response = await fetch('/api/agents')\n+ if (!response.ok) {\n+ throw new Error('Failed to fetch agents')\n+ }\n+ return await response.json()\n+ },\n+ })\n+\n+ const filteredAndSortedAgents = useMemo(() => {\n+ let filtered = agents.filter((agent) => {\n+ const matchesSearch =\n+ agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n+ agent.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||\n+ agent.tags?.some((tag) =>\n+ tag.toLowerCase().includes(searchQuery.toLowerCase())\n+ )\n+ return matchesSearch\n+ })\n+\n+ return filtered.sort((a, b) => {\n+ switch (sortBy) {\n+ case 'usage':\n+ return (b.usage_count || 0) - (a.usage_count || 0)\n+ case 'newest':\n+ return (\n+ new Date(b.created_at).getTime() - new Date(a.created_at).getTime()\n+ )\n+ case 'name':\n+ return a.name.localeCompare(b.name)\n+ case 'cost':\n+ return (b.total_spent || 0) - (a.total_spent || 0)\n+ default:\n+ return 0\n+ }\n+ })\n+ }, [agents, searchQuery, sortBy])\n+\n+ const formatCurrency = (amount?: number) => {\n+ if (!amount) return '$0.00'\n+ return `${amount.toFixed(2)}`\n+ }\n+\n+ const formatUsageCount = (count?: number) => {\n+ if (!count) return '0'\n+ if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`\n+ if (count >= 1000) return `${(count / 1000).toFixed(1)}K`\n+ return count.toString()\n+ }\n+\n+ return (\n+
\n+
\n+ {' '}\n+ {/* Header */}\n+ \n+

Agent Store

\n+

\n+ Browse all published AI agents. Run, compose, or fork them.\n+

\n+
\n+ {/* Search and Filters */}\n+ \n+
\n+
\n+ \n+ setSearchQuery(e.target.value)}\n+ className=\"pl-10\"\n+ />\n+
\n+
\n+ \n+
\n+
\n+
\n+ {/* Agent Grid */}\n+ {isLoading ? (\n+
\n+ {Array.from({ length: 6 }).map((_, i) => (\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+ \n+ \n+
\n+
\n+
\n+ ))}\n+
\n+ ) : (\n+ \n+ {filteredAndSortedAgents.map((agent, index) => (\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+ {agent.name}\n+ \n+
\n+ \n+ by @{agent.publisher.id}\n+ \n+ {agent.publisher.verified && (\n+ \n+ \u2713\n+ \n+ )}\n+
\n+
\n+ \n+
\n+
\n+ \n+

\n+ {agent.description}\n+

{' '}\n+ {/* Usage Stats */}\n+
\n+
\n+ \n+ \n+ {formatUsageCount(agent.usage_count)}\n+ \n+ uses\n+
\n+
\n+ \n+ \n+ {formatCurrency(agent.total_spent)}\n+ \n+ spent\n+
\n+
\n+ \n+ \n+ {formatCurrency(agent.avg_cost_per_invocation)}\n+ \n+ per use\n+
\n+
\n+ \n+ v{agent.version}\n+ \n+
\n+
\n+ {/* Tags */}\n+ {agent.tags && agent.tags.length > 0 && (\n+
\n+ {agent.tags.slice(0, 3).map((tag) => (\n+ \n+ {tag}\n+ \n+ ))}\n+ {agent.tags.length > 3 && (\n+ \n+ +{agent.tags.length - 3}\n+ \n+ )}\n+
\n+ )}\n+
\n+
\n+ \n+ \n+ ))}\n+ \n+ )}\n+ {filteredAndSortedAgents.length === 0 && !isLoading && (\n+ \n+
\n+ \n+

No agents found

\n+

Try adjusting your search or filter criteria

\n+
\n+
\n+ )}\n+
\n+
\n+ )\n+}\n+\n+export default AgentStorePage\n" }, { "path": "web/src/app/api/agents/route.ts", @@ -2471,8 +2468,8 @@ "id": "simplify-sdk-api", "sha": "3960e5f1b1cf7bfcddea6ef17ab4c9c9d9160c37", "parentSha": "958f2967d1a55d2666bac57cd86f36e4a6e7d652", - "spec": "Implement SDK API simplifications and exports.\n\n1) Update the SDK entrypoint exports\n- File: sdk/src/index.ts\n - Keep the existing export of CodebuffClient.\n - Remove the wildcard export that re-exports everything from './types'.\n - Add explicit exports:\n - Export WebSocketHandler from './websocket-client'.\n - Re-export getInitialSessionState from the common package at '../../common/src/types/session-state'.\n - Ensure relative paths follow existing patterns used elsewhere in the SDK (matching how other common imports are referenced).\n\n2) Relax WebSocketHandler options and solidify internal typing\n- File: sdk/src/websocket-client.ts\n - In WebSocketHandlerOptions, make the following properties optional (with ?): onWebsocketError, onWebsocketReconnect, onRequestReconnect, onResponseError, onCostResponse, onUsageResponse, onResponseChunk, onSubagentResponseChunk, onPromptResponse. Keep readFiles and handleToolCall required, and keep apiKey required.\n - Introduce type alias: type WebSocketHandlerOptionsWithDefaults = Required to represent fully-populated options with defaults.\n - Update the WebSocketHandler class’s private field types to use WebSocketHandlerOptionsWithDefaults[...] for all callback and handler properties so they are treated as non-undefined internally.\n - In the constructor parameter destructuring, keep the current no-op defaults for all now-optional callbacks (e.g., onWebsocketError = () => {}, onWebsocketReconnect = () => {}, onRequestReconnect = async () => {}, onResponseError = async () => {}, onCostResponse = async () => {}, onUsageResponse = async () => {}, onResponseChunk = async () => {}, onSubagentResponseChunk = async () => {}, onPromptResponse = async () => {}). Assign them to the corresponding private fields.\n - Ensure setupSubscriptions continues to subscribe to all action types without additional undefined checks, relying on the provided defaults.\n\n3) Acceptance criteria\n- Consumers can instantiate WebSocketHandler without providing any of the now-optional callbacks and still get correct behavior via defaults.\n- The SDK entrypoint allows importing WebSocketHandler and getInitialSessionState directly from the SDK package entry.\n- TypeScript builds pass with the stricter internal typing (no undefined callback types internally).\n- No changes to runtime behavior except allowing omitted callbacks and new entrypoint exports.", - "prompt": "Expose the primary realtime client and session initializer directly from the SDK entrypoint, and simplify the WebSocket client’s consumption by making its callback hooks optional with sensible defaults. Update typings so internals never see undefined callbacks, and ensure imports/exports align with the shared types in the common package. Keep runtime behavior consistent while reducing required boilerplate for SDK consumers.", + "spec": "Implement SDK API simplifications and exports.\n\n1) Update the SDK entrypoint exports\n- File: sdk/src/index.ts\n - Keep the existing export of CodebuffClient.\n - Remove the wildcard export that re-exports everything from './types'.\n - Add explicit exports:\n - Export WebSocketHandler from './websocket-client'.\n - Re-export getInitialSessionState from the common package at '../../common/src/types/session-state'.\n - Ensure relative paths follow existing patterns used elsewhere in the SDK (matching how other common imports are referenced).\n\n2) Relax WebSocketHandler options and solidify internal typing\n- File: sdk/src/websocket-client.ts\n - In WebSocketHandlerOptions, make the following properties optional (with ?): onWebsocketError, onWebsocketReconnect, onRequestReconnect, onResponseError, onCostResponse, onUsageResponse, onResponseChunk, onSubagentResponseChunk, onPromptResponse. Keep readFiles and handleToolCall required, and keep apiKey required.\n - Introduce type alias: type WebSocketHandlerOptionsWithDefaults = Required to represent fully-populated options with defaults.\n - Update the WebSocketHandler class\u2019s private field types to use WebSocketHandlerOptionsWithDefaults[...] for all callback and handler properties so they are treated as non-undefined internally.\n - In the constructor parameter destructuring, keep the current no-op defaults for all now-optional callbacks (e.g., onWebsocketError = () => {}, onWebsocketReconnect = () => {}, onRequestReconnect = async () => {}, onResponseError = async () => {}, onCostResponse = async () => {}, onUsageResponse = async () => {}, onResponseChunk = async () => {}, onSubagentResponseChunk = async () => {}, onPromptResponse = async () => {}). Assign them to the corresponding private fields.\n - Ensure setupSubscriptions continues to subscribe to all action types without additional undefined checks, relying on the provided defaults.\n\n3) Acceptance criteria\n- Consumers can instantiate WebSocketHandler without providing any of the now-optional callbacks and still get correct behavior via defaults.\n- The SDK entrypoint allows importing WebSocketHandler and getInitialSessionState directly from the SDK package entry.\n- TypeScript builds pass with the stricter internal typing (no undefined callback types internally).\n- No changes to runtime behavior except allowing omitted callbacks and new entrypoint exports.", + "prompt": "Expose the primary realtime client and session initializer directly from the SDK entrypoint, and simplify the WebSocket client\u2019s consumption by making its callback hooks optional with sensible defaults. Update typings so internals never see undefined callbacks, and ensure imports/exports align with the shared types in the common package. Keep runtime behavior consistent while reducing required boilerplate for SDK consumers.", "supplementalFiles": [ "common/src/types/session-state.ts", "common/src/actions.ts", @@ -2496,7 +2493,7 @@ "id": "add-input-apis", "sha": "958f2967d1a55d2666bac57cd86f36e4a6e7d652", "parentSha": "39743331b85a721408dec421396911b12b1de099", - "spec": "Implement the following changes across the specified files:\n\n1) common/src/actions.ts\n- Remove the 'generate-commit-message' client action variant from the CLIENT_ACTION_SCHEMA discriminated union. Do not leave any references to it in this schema.\n- Ensure the remaining client actions still include: 'prompt', 'read-files-response', 'init', 'tool-call-response', and 'cancel-user-input' with their existing shapes.\n- Do not change server action schemas or other client action variants.\n\n2) sdk/src/websocket-client.ts\n- Extend WebSocketHandlerOptions to include an `apiKey: string` property, and store it in the WebSocketHandler instance (private field).\n- In the constructor, accept the apiKey option and assign it to the private field. Keep existing subscriptions and event handlers intact.\n- Remove the init() method that previously sent the 'init' action and instead provide methods dedicated to input lifecycle:\n a) Add a private helper getInputDefaultOptions() that returns an object containing defaults for a user prompt send, including:\n - type: 'prompt'\n - fingerprintId: 'codebuff-sdk'\n - authToken: this.apiKey\n b) Add a public method sendInput(action) that sends a 'prompt' action using the underlying websocket by merging the caller-provided fields with the defaults from getInputDefaultOptions(). The method should accept an input typed as the 'prompt' ClientAction minus the keys supplied by the defaults (type, fingerprintId, authToken).\n c) Add a public method cancelInput({ promptId }) that sends a 'cancel-user-input' action including `authToken: this.apiKey` and the provided promptId.\n- Retain all existing subscription setup for handling 'response-chunk', 'subagent-response-chunk', 'prompt-response', 'usage-response', 'message-cost-response', 'tool-call-request/response', and 'request-reconnect'.\n- Do not alter APIRealtimeClient behavior.\n\nConstraints/notes:\n- Do not modify SDK exports or other files beyond what’s listed; keep changes scoped to the two files above.\n- The server already expects 'prompt' and 'cancel-user-input' with authToken and promptId; the new SDK methods must populate these fields accordingly.\n- No other cleanup is needed for the removed 'generate-commit-message' action, as there are no references elsewhere.", + "spec": "Implement the following changes across the specified files:\n\n1) common/src/actions.ts\n- Remove the 'generate-commit-message' client action variant from the CLIENT_ACTION_SCHEMA discriminated union. Do not leave any references to it in this schema.\n- Ensure the remaining client actions still include: 'prompt', 'read-files-response', 'init', 'tool-call-response', and 'cancel-user-input' with their existing shapes.\n- Do not change server action schemas or other client action variants.\n\n2) sdk/src/websocket-client.ts\n- Extend WebSocketHandlerOptions to include an `apiKey: string` property, and store it in the WebSocketHandler instance (private field).\n- In the constructor, accept the apiKey option and assign it to the private field. Keep existing subscriptions and event handlers intact.\n- Remove the init() method that previously sent the 'init' action and instead provide methods dedicated to input lifecycle:\n a) Add a private helper getInputDefaultOptions() that returns an object containing defaults for a user prompt send, including:\n - type: 'prompt'\n - fingerprintId: 'codebuff-sdk'\n - authToken: this.apiKey\n b) Add a public method sendInput(action) that sends a 'prompt' action using the underlying websocket by merging the caller-provided fields with the defaults from getInputDefaultOptions(). The method should accept an input typed as the 'prompt' ClientAction minus the keys supplied by the defaults (type, fingerprintId, authToken).\n c) Add a public method cancelInput({ promptId }) that sends a 'cancel-user-input' action including `authToken: this.apiKey` and the provided promptId.\n- Retain all existing subscription setup for handling 'response-chunk', 'subagent-response-chunk', 'prompt-response', 'usage-response', 'message-cost-response', 'tool-call-request/response', and 'request-reconnect'.\n- Do not alter APIRealtimeClient behavior.\n\nConstraints/notes:\n- Do not modify SDK exports or other files beyond what\u2019s listed; keep changes scoped to the two files above.\n- The server already expects 'prompt' and 'cancel-user-input' with authToken and promptId; the new SDK methods must populate these fields accordingly.\n- No other cleanup is needed for the removed 'generate-commit-message' action, as there are no references elsewhere.", "prompt": "Add high-level SDK support for sending and canceling user inputs over WebSocket. Provide methods to submit a user prompt (including default metadata like fingerprint and auth) and to cancel an in-flight prompt using its ID. Also remove any unused client action related to commit message generation from the shared action schema, ensuring only the supported client actions remain.", "supplementalFiles": [ "backend/src/websockets/websocket-action.ts", @@ -2555,7 +2552,7 @@ { "path": "sdk/package.json", "status": "modified", - "diff": "Index: sdk/package.json\n===================================================================\n--- sdk/package.json\te79f36b (parent)\n+++ sdk/package.json\ta9fe09f (commit)\n@@ -1,9 +1,9 @@\n {\n \"name\": \"@codebuff/sdk\",\n \"private\": false,\n \"access\": \"public\",\n- \"version\": \"0.0.3\",\n+ \"version\": \"0.1.0\",\n \"description\": \"Official SDK for Codebuff — AI coding agent & framework\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n" + "diff": "Index: sdk/package.json\n===================================================================\n--- sdk/package.json\te79f36b (parent)\n+++ sdk/package.json\ta9fe09f (commit)\n@@ -1,9 +1,9 @@\n {\n \"name\": \"@codebuff/sdk\",\n \"private\": false,\n \"access\": \"public\",\n- \"version\": \"0.0.3\",\n+ \"version\": \"0.1.0\",\n \"description\": \"Official SDK for Codebuff \u2014 AI coding agent & framework\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n" }, { "path": "sdk/src/client.ts", @@ -2583,7 +2580,7 @@ "id": "new-account-banner", "sha": "e79f36b22994fed995e5e4f2f9dbe01d7d4b9f3e", "parentSha": "a7841066e230e221b94c9ed1e6c25b0e3aab0fca", - "spec": "Implement a one-week age gate for the referral banner based on the user's account creation date.\n\nRequired changes:\n1) API: web/src/app/api/user/profile/route.ts\n- Extend the user query to include created_at in the selected columns.\n- Add created_at to the JSON response object so the frontend can consume it.\n- Preserve existing fields and logic (auto_topup_* and blocked_reason). Return created_at as a serializable value.\n\n2) Types: web/src/types/user.ts\n- Extend the UserProfile interface to include created_at: Date | null to reflect the frontend usage. This will be populated by the new hook (converted from the API’s serialized value).\n\n3) Frontend data hook: web/src/hooks/use-user-profile.ts (new file)\n- Create a React Query hook that fetches /api/user/profile when a user session exists.\n- On successful fetch, convert created_at (if present as a string) into a Date.\n- Cache the profile in localStorage under a stable key and hydrate initialData from it; clear this cache on user logout.\n- Expose a clearCache helper in the returned result.\n- Use a distinct query key (e.g., ['user-profile']).\n\n4) Banner visibility: web/src/components/ui/banner.tsx\n- Import and use the new useUserProfile hook.\n- Compute isNewAccount as true when created_at exists and is within the last 7 days; otherwise false.\n- Only render the banner when the component is visible, a session exists, the user profile is loaded, and isNewAccount is true.\n- Keep existing referral detection via search params and PostHog tracking intact.\n\nBehavioral outcomes:\n- For accounts created within the last 7 days, the referral banner displays as before.\n- For accounts older than 7 days, the banner does not render.\n- If created_at is absent or the user is not authenticated, the banner does not render.\n- User profile is efficiently cached client-side and survives soft navigations; cache clears on logout.\n", + "spec": "Implement a one-week age gate for the referral banner based on the user's account creation date.\n\nRequired changes:\n1) API: web/src/app/api/user/profile/route.ts\n- Extend the user query to include created_at in the selected columns.\n- Add created_at to the JSON response object so the frontend can consume it.\n- Preserve existing fields and logic (auto_topup_* and blocked_reason). Return created_at as a serializable value.\n\n2) Types: web/src/types/user.ts\n- Extend the UserProfile interface to include created_at: Date | null to reflect the frontend usage. This will be populated by the new hook (converted from the API\u2019s serialized value).\n\n3) Frontend data hook: web/src/hooks/use-user-profile.ts (new file)\n- Create a React Query hook that fetches /api/user/profile when a user session exists.\n- On successful fetch, convert created_at (if present as a string) into a Date.\n- Cache the profile in localStorage under a stable key and hydrate initialData from it; clear this cache on user logout.\n- Expose a clearCache helper in the returned result.\n- Use a distinct query key (e.g., ['user-profile']).\n\n4) Banner visibility: web/src/components/ui/banner.tsx\n- Import and use the new useUserProfile hook.\n- Compute isNewAccount as true when created_at exists and is within the last 7 days; otherwise false.\n- Only render the banner when the component is visible, a session exists, the user profile is loaded, and isNewAccount is true.\n- Keep existing referral detection via search params and PostHog tracking intact.\n\nBehavioral outcomes:\n- For accounts created within the last 7 days, the referral banner displays as before.\n- For accounts older than 7 days, the banner does not render.\n- If created_at is absent or the user is not authenticated, the banner does not render.\n- User profile is efficiently cached client-side and survives soft navigations; cache clears on logout.\n", "prompt": "Show the referral banner only for new users. Expose the account creation date from the user profile API, add a frontend hook to fetch and cache the profile, and update the banner to render only when the account is less than a week old. Keep existing referral behavior and analytics intact.", "supplementalFiles": [ "common/src/db/schema.ts", @@ -2621,7 +2618,7 @@ "sha": "a7841066e230e221b94c9ed1e6c25b0e3aab0fca", "parentSha": "5daa4424303a0c6416051083e73e6eb69e37e262", "spec": "Implement three coordinated changes:\n\n1) Preserve subagents when --agent is specified\n- File: backend/src/main-prompt.ts\n- Behavior: If the prompt action includes a CLI-specified agentId, do not modify that agent's subagents array. Only update/expand subagents when no agentId was provided.\n- Implementation details:\n - Initialize updatedSubagents to mainAgentTemplate.subagents.\n - If agentId is not set, set updatedSubagents to either fileContext.codebuffConfig?.subagents (when present) or the union of mainAgentTemplate.subagents and availableAgents (deduped).\n - Assign mainAgentTemplate.subagents = updatedSubagents and persist in localAgentTemplates.\n\n2) Always load and display local agents on CLI startup\n- File: npm-app/src/index.ts\n- Behavior: Unconditionally load local agents and display the configured/loaded agents in the CLI startup logs, regardless of whether --agent was passed.\n- Implementation details:\n - Remove the conditional guard that previously wrapped loadLocalAgents/displayLoadedAgents with if (!agent).\n - Ensure loadLocalAgents({ verbose: true }) runs and then displayLoadedAgents(loadCodebuffConfig()) is called.\n\n3) Normalize file-explorer subagent ID to local identifier\n- File: .agents/file-explorer.ts\n- Behavior: Update the subagents list to reference the local agent id 'file-picker' instead of a publisher/version-scoped id. This ensures compatibility with the spawn_agents allowlist validation and local agent resolution.\n- Implementation details:\n - Replace subagents: [`codebuff/file-picker@${version}`] with subagents: [`file-picker`].\n\nAcceptance criteria:\n- Running the CLI with --agent preserves that agent's subagents as authored (no merging with codebuff.json or all available local agents).\n- Running the CLI without --agent continues to apply codebuff.json.subagents when present, otherwise merges all available local agents into the main agent's subagents.\n- On startup (with or without --agent), the CLI logs configured base agent and/or configured subagents or found custom agents as before.\n- Spawning from the file-explorer agent successfully resolves and allows 'file-picker' as a subagent without requiring a publisher/version-qualified id.", - "prompt": "Update the agent selection and loading behavior so that choosing a specific agent via the CLI does not alter that agent’s subagent allowlist. When no agent is specified, keep the current behavior of using subagents from the project config or falling back to all local agents. Ensure the CLI always loads and displays local agents on startup for discoverability. Also align the file-explorer agent to reference the local file picker subagent by its simple id, not a publisher/version-qualified id.", + "prompt": "Update the agent selection and loading behavior so that choosing a specific agent via the CLI does not alter that agent\u2019s subagent allowlist. When no agent is specified, keep the current behavior of using subagents from the project config or falling back to all local agents. Ensure the CLI always loads and displays local agents on startup for discoverability. Also align the file-explorer agent to reference the local file picker subagent by its simple id, not a publisher/version-qualified id.", "supplementalFiles": [ "npm-app/src/agents/load-agents.ts", "npm-app/src/cli-definitions.ts", @@ -2653,7 +2650,7 @@ "id": "unify-tool-types", "sha": "2c7027715652da5cc87e54e1c87883d44ae954f2", "parentSha": "59eaafe6974950d73a7c9c561e330bd593bfc241", - "spec": "Implement the following cohesive updates across agents, types, rendering, and tests:\n\n1) Update open-source agent models\n- File: .agents/opensource/researcher.ts\n - Set model to 'z-ai/glm-4.5:fast'.\n- File: .agents/opensource/thinker.ts\n - Set model to 'qwen/qwen3-235b-a22b-thinking-2507:fast'.\n\n2) Align agent config typing for handleSteps in agent template types (agents template copy)\n- File: .agents/types/agent-config.d.ts\n - In the generator return type for handleSteps, change the third generic (the yielded back value) from `{ agentState: AgentState; toolResult: string | undefined }` to `{ agentState: AgentState; toolResult: ToolResult | undefined }`.\n - In the example JSDoc above handleSteps, simplify the step loop to `yield 'STEP'` (remove the sample code that inspects `toolResult?.toolName === 'end_turn'`).\n\n3) Normalize tool parameter type declarations for agent templates (agents template copy)\n- File: .agents/types/tools.d.ts\n - Keep the ToolName union unchanged in meaning but present it in compact single-line form.\n - For ToolParamsMap keys, use quoted string literal keys (e.g., 'add_message') for consistency with downstream JSON schema generation.\n - For all tool param interfaces, change fields to quoted property names (e.g., \"role\", \"content\", etc.) for JSON-like clarity and consistency.\n - Define EndTurnParams and SetOutputParams as explicit empty interfaces with braces rather than empty type aliases.\n - Do not change any tool semantics; this is a typing/formatting normalization.\n\n4) Remove transport-only flag from common tool param types\n- File: common/src/util/types/tools.d.ts\n - Remove any reference to \"cb_easp\" from tool parameter interfaces (specifically from CodeSearchParams). This flag is a transport parameter and should not appear in the tool params types.\n\n5) Refactor CLI tool renderers for spawn agents\n- File: npm-app/src/utils/tool-renderers.ts\n - Remove import and usage of AGENT_PERSONAS.\n - Introduce a shared helper `renderSpawnAgentsParam(paramName, toolName, content)` that:\n - When paramName is 'agents', parses the JSON content into an array of objects with fields { agent_type, prompt, params? }.\n - Resolves each agent display name from Client.getInstance(false)?.agentNames[agent_type]; when missing, fall back to the raw agent_type string.\n - Returns a formatted, gray text block where each agent is rendered as `@${bold(agentName)}:\\n${prompt || 'No prompt provided'}`, joined by a blank line, and ending with a newline. Return null if content cannot be parsed or empty.\n - Use this helper for both spawn_agents and spawn_agents_async renderers' onParamEnd.\n - Keep onToolStart to render \"[Spawn Agents]\" and onToolEnd to start the Spinner with \"Agents running...\" unchanged.\n\n6) Harden read_docs tests to avoid network and improve determinism\n- File: backend/src/__tests__/read-docs-tool.test.ts\n - In tests that fetch documentation (including the basic query and topic/max_tokens variants), mock context7Api.searchLibraries to return a single library object with plausible fields (e.g., id/title/description/branch/lastUpdateDate/state/totalTokens/totalSnippets/totalPages) before mocking fetchContext7LibraryDocumentation.\n - In the \"should handle case when no documentation is found\" test, also mock searchLibraries to return an empty array to ensure the handler returns the no-documentation message without network calls.\n - For error-path tests (API errors, non-Error exceptions), mock searchLibraries to return a valid library list so the doc fetch path is exercised deterministically prior to throwing in fetchContext7LibraryDocumentation.\n\nAcceptance criteria:\n- All modified types compile across the monorepo, and the agent-builder’s inclusion of these .d.ts files still works.\n- npm-app spawn_agents and spawn_agents_async rendering shows dynamic agent display names when available and otherwise shows the raw agent type; no static AGENT_PERSONAS fallback is used.\n- The tests in backend/src/__tests__/read-docs-tool.test.ts run without attempting any network calls to Context7 and pass deterministically.\n- No tool parameter interface includes cb_easp in common/src/util/types/tools.d.ts.\n", + "spec": "Implement the following cohesive updates across agents, types, rendering, and tests:\n\n1) Update open-source agent models\n- File: .agents/opensource/researcher.ts\n - Set model to 'z-ai/glm-4.5:fast'.\n- File: .agents/opensource/thinker.ts\n - Set model to 'qwen/qwen3-235b-a22b-thinking-2507:fast'.\n\n2) Align agent config typing for handleSteps in agent template types (agents template copy)\n- File: .agents/types/agent-config.d.ts\n - In the generator return type for handleSteps, change the third generic (the yielded back value) from `{ agentState: AgentState; toolResult: string | undefined }` to `{ agentState: AgentState; toolResult: ToolResult | undefined }`.\n - In the example JSDoc above handleSteps, simplify the step loop to `yield 'STEP'` (remove the sample code that inspects `toolResult?.toolName === 'end_turn'`).\n\n3) Normalize tool parameter type declarations for agent templates (agents template copy)\n- File: .agents/types/tools.d.ts\n - Keep the ToolName union unchanged in meaning but present it in compact single-line form.\n - For ToolParamsMap keys, use quoted string literal keys (e.g., 'add_message') for consistency with downstream JSON schema generation.\n - For all tool param interfaces, change fields to quoted property names (e.g., \"role\", \"content\", etc.) for JSON-like clarity and consistency.\n - Define EndTurnParams and SetOutputParams as explicit empty interfaces with braces rather than empty type aliases.\n - Do not change any tool semantics; this is a typing/formatting normalization.\n\n4) Remove transport-only flag from common tool param types\n- File: common/src/util/types/tools.d.ts\n - Remove any reference to \"cb_easp\" from tool parameter interfaces (specifically from CodeSearchParams). This flag is a transport parameter and should not appear in the tool params types.\n\n5) Refactor CLI tool renderers for spawn agents\n- File: npm-app/src/utils/tool-renderers.ts\n - Remove import and usage of AGENT_PERSONAS.\n - Introduce a shared helper `renderSpawnAgentsParam(paramName, toolName, content)` that:\n - When paramName is 'agents', parses the JSON content into an array of objects with fields { agent_type, prompt, params? }.\n - Resolves each agent display name from Client.getInstance(false)?.agentNames[agent_type]; when missing, fall back to the raw agent_type string.\n - Returns a formatted, gray text block where each agent is rendered as `@${bold(agentName)}:\\n${prompt || 'No prompt provided'}`, joined by a blank line, and ending with a newline. Return null if content cannot be parsed or empty.\n - Use this helper for both spawn_agents and spawn_agents_async renderers' onParamEnd.\n - Keep onToolStart to render \"[Spawn Agents]\" and onToolEnd to start the Spinner with \"Agents running...\" unchanged.\n\n6) Harden read_docs tests to avoid network and improve determinism\n- File: backend/src/__tests__/read-docs-tool.test.ts\n - In tests that fetch documentation (including the basic query and topic/max_tokens variants), mock context7Api.searchLibraries to return a single library object with plausible fields (e.g., id/title/description/branch/lastUpdateDate/state/totalTokens/totalSnippets/totalPages) before mocking fetchContext7LibraryDocumentation.\n - In the \"should handle case when no documentation is found\" test, also mock searchLibraries to return an empty array to ensure the handler returns the no-documentation message without network calls.\n - For error-path tests (API errors, non-Error exceptions), mock searchLibraries to return a valid library list so the doc fetch path is exercised deterministically prior to throwing in fetchContext7LibraryDocumentation.\n\nAcceptance criteria:\n- All modified types compile across the monorepo, and the agent-builder\u2019s inclusion of these .d.ts files still works.\n- npm-app spawn_agents and spawn_agents_async rendering shows dynamic agent display names when available and otherwise shows the raw agent type; no static AGENT_PERSONAS fallback is used.\n- The tests in backend/src/__tests__/read-docs-tool.test.ts run without attempting any network calls to Context7 and pass deterministically.\n- No tool parameter interface includes cb_easp in common/src/util/types/tools.d.ts.\n", "prompt": "Bring agent, type, and rendering behavior into alignment across the project. Update the open-source researcher and thinker agents to use the latest intended models. Normalize and modernize the agent template and tool parameter type definitions so they reflect real runtime structures and avoid transport-only flags. Unify the spawn agents rendering to prefer dynamic agent names provided by the client and gracefully fall back when unknown, without relying on static personas. Finally, make the read_docs tests deterministic by stubbing the library search so no network calls occur.", "supplementalFiles": [ "common/src/constants/agents.ts", @@ -2706,7 +2703,7 @@ "sha": "59eaafe6974950d73a7c9c561e330bd593bfc241", "parentSha": "a0ae42629f444703695b351e46f48198539e3003", "spec": "Implement the following changes across the specified files:\n\n1) Validate DB agents with short ID, then set full ID (backend/src/templates/agent-registry.ts)\n- In fetchAgentFromDatabase, change validation to call validateSingleAgent using the raw agent data with id set to the original agentId (the short slug, without publisher or version). Pass filePath as \"publisherId/agentId@version\" and set skipSubagentValidation: true.\n- After a successful validation, construct the final AgentTemplate by copying validationResult.agentTemplate and overriding id to the full identifier: \"publisherId/agentId@version\".\n- Update logging:\n - On validation error: remove logging of fullAgentId; keep publisherId, agentId, version, and error.\n - On success: log fullAgentId using the final agentTemplate.id and omit logging the entire agentConfig object.\n- Return the final agentTemplate instead of validationResult.agentTemplate.\n\n2) Only load local agents when no agent is specified and avoid early config reference (npm-app/src/index.ts)\n- Introduce a promise (e.g., loadLocalAgentsPromise) that resolves immediately; if no specific agent is requested (i.e., agent is falsy), inside that promise call loadLocalAgents({ verbose: true }) and then, after it resolves, load the Codebuff config via loadCodebuffConfig() and call displayLoadedAgents with it.\n- Replace the existing unconditional loadLocalAgents(...) in the readyPromise with the new conditional loadLocalAgentsPromise.\n- Ensure there is no reference to codebuffConfig before it is defined; do not use a top-level codebuffConfig variable in the readyPromise chain.\n- Preserve the existing initialization flow and CLI initialization parameters.\n\n3) Readability tweak (backend/src/websockets/websocket-action.ts)\n- Split the destructuring assignment that gets localAgentTemplates from assembleLocalAgentTemplates(fileContext) across multiple lines for readability without changing behavior.\n\nAcceptance criteria:\n- Validating a DB agent with an ID containing only lowercase letters, numbers, and hyphens succeeds; the final returned AgentTemplate has id in the full \"publisher/agent@version\" format.\n- Error logs on validation failure no longer include a fullAgentId field; success logs include fullAgentId matching the final AgentTemplate.id and do not include the raw agentConfig.\n- When starting the CLI with --agent set, local agents are not loaded or displayed; when --agent is not set, local agents are loaded and displayed after reading the config, and there are no references to config variables before they are initialized.\n- websocket-action formatting change compiles and has no functional impact.", - "prompt": "Refactor the agent loading and validation flow.\n\nBackend: When fetching an agent from the database, validate the raw template using the simple agent ID (not the composite publisher/agent@version) to satisfy the schema, then set the full composite ID on the final template before returning it. Adjust logs accordingly so validation errors don’t log a full ID and successes log the correct full ID.\n\nCLI: Load local agents only when no specific --agent is requested. Ensure the configuration is loaded at the right time and avoid referencing it before it exists. Display loaded agents only after the config is read in that conditional path. Keep the overall startup sequence intact.\n\nAlso, apply a small readability improvement to the assembleLocalAgentTemplates destructuring in the WebSocket action without changing behavior.", + "prompt": "Refactor the agent loading and validation flow.\n\nBackend: When fetching an agent from the database, validate the raw template using the simple agent ID (not the composite publisher/agent@version) to satisfy the schema, then set the full composite ID on the final template before returning it. Adjust logs accordingly so validation errors don\u2019t log a full ID and successes log the correct full ID.\n\nCLI: Load local agents only when no specific --agent is requested. Ensure the configuration is loaded at the right time and avoid referencing it before it exists. Display loaded agents only after the config is read in that conditional path. Keep the overall startup sequence intact.\n\nAlso, apply a small readability improvement to the assembleLocalAgentTemplates destructuring in the WebSocket action without changing behavior.", "supplementalFiles": [ "common/src/templates/agent-validation.ts", "common/src/types/dynamic-agent-template.ts", @@ -2737,7 +2734,7 @@ "id": "agents-cleanup", "sha": "b748a06b88e1f6f34504479714a4c44e9392e0e1", "parentSha": "e056a236d1bcd869ab94c05f25d9fe02ec91e69b", - "spec": "Implement the following changes across the agent templates:\n\n1) Add a new Agent Builder template\n- File to create: .agents/agent-builder.ts\n- Defines an AgentConfig for id \"agent-builder\" (displayName: \"Bob the Agent Builder\"), model \"anthropic/claude-4-sonnet-20250522\", toolNames: [\"write_file\", \"str_replace\", \"run_terminal_command\", \"read_files\", \"code_search\", \"spawn_agents\", \"add_message\", \"end_turn\"], subagents: [`codebuff/file-picker@${version}`], includeMessageHistory: false, with parent/system/instructions prompts describing its purpose and best practices.\n- Import publisher and version from ./.agents/constants and import type { AgentConfig } from \"./types/agent-config\".\n- handleSteps generator must:\n a) Ensure .agents/types directory exists by running a synchronous mkdir -p with a reasonable timeout.\n b) Read type definitions from the monorepo and write local copies under .agents/types:\n - Read common/src/util/types/agent-config.d.ts and write to .agents/types/agent-config.d.ts\n - Read common/src/util/types/tools.d.ts and write to .agents/types/tools.d.ts\n c) Copy example agents into .agents for user reference by reading each file and writing to the corresponding destination:\n - common/src/util/example-1.ts -> .agents/example-1.ts\n - common/src/util/example-2.ts -> .agents/example-2.ts\n - common/src/util/example-3.ts -> .agents/example-3.ts\n d) Yield STEP_ALL to let the model ask clarifying questions or continue after scaffolding.\n\n2) Fix tool result handling where results were treated as objects\n- .agents/changes-reviewer.ts: Treat tool results as strings.\n • Replace usage of gitDiffResult?.result with gitDiffResult (fallback to empty string as needed).\n • Replace gitStatusResult?.result similarly.\n- .agents/file-explorer.ts: When calling set_output, pass the tool result string directly.\n • Change results: spawnResult?.result to results: spawnResult.\n- .agents/claude4-gemini-thinking.ts: Remove checks that treat toolResult as an object with a toolName.\n • Remove the destructured thinkResult and the if (thinkResult?.toolName === 'end_turn') condition; simply yield 'STEP' in the loop.\n\n3) Simplify prompts and step handling for specific agents\n- .agents/file-picker.ts:\n • Remove unused placeholder prompt blocks (e.g., {CODEBUFF_TOOLS_PROMPT}, {CODEBUFF_AGENTS_PROMPT}).\n • In handleSteps, do not capture the tool result variable; just yield the find_files tool and then STEP_ALL.\n- .agents/git-committer.ts:\n • Simplify toolNames to [\"read_files\", \"run_terminal_command\", \"add_message\", \"end_turn\"].\n • Remove outputSchema (and the requirement to use set_output).\n • Remove stepPrompt that instructed using set_output.\n- .agents/planner.ts:\n • Replace systemPrompt with a concise version stating it creates comprehensive plans (no placeholders), remove stepPrompt.\n- .agents/researcher.ts:\n • Simplify systemPrompt to end with \"Always end your response with the end_turn tool.\" and set stepPrompt to \"Don't forget to end your response with the end_turn tool.\".\n- .agents/superagent.ts:\n • Simplify systemPrompt to a concise version without placeholder blocks.\n\nNotes and constraints\n- Do not introduce code that expects tool results to be objects in handleSteps; treat toolResult as a plain string.\n- Use the correct type source paths under common/src/util/types for agent-config.d.ts and tools.d.ts when scaffolding types in the new Agent Builder.\n- Avoid reintroducing placeholder tokens (e.g., {CODEBUFF_TOOLS_PROMPT}) in systemPrompt content for the affected agents.\n- Keep existing behavior and intent of each agent intact while applying the cleanup above.", + "spec": "Implement the following changes across the agent templates:\n\n1) Add a new Agent Builder template\n- File to create: .agents/agent-builder.ts\n- Defines an AgentConfig for id \"agent-builder\" (displayName: \"Bob the Agent Builder\"), model \"anthropic/claude-4-sonnet-20250522\", toolNames: [\"write_file\", \"str_replace\", \"run_terminal_command\", \"read_files\", \"code_search\", \"spawn_agents\", \"add_message\", \"end_turn\"], subagents: [`codebuff/file-picker@${version}`], includeMessageHistory: false, with parent/system/instructions prompts describing its purpose and best practices.\n- Import publisher and version from ./.agents/constants and import type { AgentConfig } from \"./types/agent-config\".\n- handleSteps generator must:\n a) Ensure .agents/types directory exists by running a synchronous mkdir -p with a reasonable timeout.\n b) Read type definitions from the monorepo and write local copies under .agents/types:\n - Read common/src/util/types/agent-config.d.ts and write to .agents/types/agent-config.d.ts\n - Read common/src/util/types/tools.d.ts and write to .agents/types/tools.d.ts\n c) Copy example agents into .agents for user reference by reading each file and writing to the corresponding destination:\n - common/src/util/example-1.ts -> .agents/example-1.ts\n - common/src/util/example-2.ts -> .agents/example-2.ts\n - common/src/util/example-3.ts -> .agents/example-3.ts\n d) Yield STEP_ALL to let the model ask clarifying questions or continue after scaffolding.\n\n2) Fix tool result handling where results were treated as objects\n- .agents/changes-reviewer.ts: Treat tool results as strings.\n \u2022 Replace usage of gitDiffResult?.result with gitDiffResult (fallback to empty string as needed).\n \u2022 Replace gitStatusResult?.result similarly.\n- .agents/file-explorer.ts: When calling set_output, pass the tool result string directly.\n \u2022 Change results: spawnResult?.result to results: spawnResult.\n- .agents/claude4-gemini-thinking.ts: Remove checks that treat toolResult as an object with a toolName.\n \u2022 Remove the destructured thinkResult and the if (thinkResult?.toolName === 'end_turn') condition; simply yield 'STEP' in the loop.\n\n3) Simplify prompts and step handling for specific agents\n- .agents/file-picker.ts:\n \u2022 Remove unused placeholder prompt blocks (e.g., {CODEBUFF_TOOLS_PROMPT}, {CODEBUFF_AGENTS_PROMPT}).\n \u2022 In handleSteps, do not capture the tool result variable; just yield the find_files tool and then STEP_ALL.\n- .agents/git-committer.ts:\n \u2022 Simplify toolNames to [\"read_files\", \"run_terminal_command\", \"add_message\", \"end_turn\"].\n \u2022 Remove outputSchema (and the requirement to use set_output).\n \u2022 Remove stepPrompt that instructed using set_output.\n- .agents/planner.ts:\n \u2022 Replace systemPrompt with a concise version stating it creates comprehensive plans (no placeholders), remove stepPrompt.\n- .agents/researcher.ts:\n \u2022 Simplify systemPrompt to end with \"Always end your response with the end_turn tool.\" and set stepPrompt to \"Don't forget to end your response with the end_turn tool.\".\n- .agents/superagent.ts:\n \u2022 Simplify systemPrompt to a concise version without placeholder blocks.\n\nNotes and constraints\n- Do not introduce code that expects tool results to be objects in handleSteps; treat toolResult as a plain string.\n- Use the correct type source paths under common/src/util/types for agent-config.d.ts and tools.d.ts when scaffolding types in the new Agent Builder.\n- Avoid reintroducing placeholder tokens (e.g., {CODEBUFF_TOOLS_PROMPT}) in systemPrompt content for the affected agents.\n- Keep existing behavior and intent of each agent intact while applying the cleanup above.", "prompt": "Create a new agent that scaffolds agent templates and related type definitions, then streamline several existing agents to align with the current tool result behavior and simplified prompts. The builder should set up a local types folder under .agents, copy example templates for reference, and prepare the environment for creating or editing new agents. For the existing agents, remove placeholder prompt blocks, eliminate any reliance on object-shaped tool results, and simplify prompts while preserving intended functionality.", "supplementalFiles": [ "npm-app/src/tool-handlers.ts", @@ -2856,7 +2853,7 @@ "id": "enforce-agent-tools", "sha": "8b6285b273edd2a45bd3222c5c458149fd4a41d1", "parentSha": "bb61b285c5bab3bc02a01c434a4ea09b6f0749ae", - "spec": "Implement stricter validation for dynamic agent templates and add corresponding tests.\n\nChanges to make:\n1) Schema refinements\n- File: common/src/types/dynamic-agent-template.ts\n - Add a refinement that rejects templates if toolNames includes 'set_output' while outputMode is not 'json'.\n • Condition: data.toolNames.includes('set_output') && data.outputMode !== 'json'\n • Error message: ''set_output' tool requires outputMode to be 'json'. Change outputMode to 'json' or remove 'set_output' from toolNames.'\n • Path: ['outputMode']\n - Add a refinement that rejects templates if subagents is non-empty but 'spawn_agents' is not included in toolNames.\n • Condition: data.subagents.length > 0 && !data.toolNames.includes('spawn_agents')\n • Error message: 'Non-empty subagents array requires the 'spawn_agents' tool. Add 'spawn_agents' to toolNames or remove subagents.'\n • Path: ['toolNames']\n - Preserve existing refinements (outputSchema => json, json => set_output) and default behaviors.\n\n2) Tests\n- File: common/src/__tests__/agent-validation.test.ts\n - Add a test named: 'should reject set_output tool without json output mode'.\n • Construct an agent config with outputMode: 'last_message' and toolNames including 'set_output'.\n • Validate via DynamicAgentTemplateSchema.safeParse and assert failure.\n • Assert the error message contains: ''set_output' tool requires outputMode to be 'json''.\n\n- File: common/src/__tests__/dynamic-agent-template-schema.test.ts\n - Add test: 'should reject template with set_output tool but non-json outputMode'.\n • Build from valid base template; set outputMode: 'last_message' and include 'set_output'. Expect failure and verify message includes ''set_output' tool requires outputMode to be 'json''.\n - Add test: 'should reject template with set_output tool and all_messages outputMode'.\n • Same as above but with outputMode: 'all_messages'. Expect failure.\n - Add test: 'should reject template with non-empty subagents but missing spawn_agents tool'.\n • Set subagents to a non-empty array and toolNames not including 'spawn_agents'. Expect failure and message to include: 'Non-empty subagents array requires the 'spawn_agents' tool'.\n - Add test: 'should accept template with non-empty subagents and spawn_agents tool'.\n • Provide subagents and include 'spawn_agents' in toolNames. Expect success.\n - Add test: 'should accept template with empty subagents and no spawn_agents tool'.\n • Provide empty subagents and omit 'spawn_agents'. Expect success.\n\nBehavioral expectations:\n- Any template that includes 'set_output' must use outputMode 'json'.\n- Any template with one or more subagents must include the 'spawn_agents' tool.\n- Existing constraints (e.g., json mode requires set_output and outputSchema => json) remain enforced.\n- Error messages must match the specified strings so tests can assert on them.\n", + "spec": "Implement stricter validation for dynamic agent templates and add corresponding tests.\n\nChanges to make:\n1) Schema refinements\n- File: common/src/types/dynamic-agent-template.ts\n - Add a refinement that rejects templates if toolNames includes 'set_output' while outputMode is not 'json'.\n \u2022 Condition: data.toolNames.includes('set_output') && data.outputMode !== 'json'\n \u2022 Error message: ''set_output' tool requires outputMode to be 'json'. Change outputMode to 'json' or remove 'set_output' from toolNames.'\n \u2022 Path: ['outputMode']\n - Add a refinement that rejects templates if subagents is non-empty but 'spawn_agents' is not included in toolNames.\n \u2022 Condition: data.subagents.length > 0 && !data.toolNames.includes('spawn_agents')\n \u2022 Error message: 'Non-empty subagents array requires the 'spawn_agents' tool. Add 'spawn_agents' to toolNames or remove subagents.'\n \u2022 Path: ['toolNames']\n - Preserve existing refinements (outputSchema => json, json => set_output) and default behaviors.\n\n2) Tests\n- File: common/src/__tests__/agent-validation.test.ts\n - Add a test named: 'should reject set_output tool without json output mode'.\n \u2022 Construct an agent config with outputMode: 'last_message' and toolNames including 'set_output'.\n \u2022 Validate via DynamicAgentTemplateSchema.safeParse and assert failure.\n \u2022 Assert the error message contains: ''set_output' tool requires outputMode to be 'json''.\n\n- File: common/src/__tests__/dynamic-agent-template-schema.test.ts\n - Add test: 'should reject template with set_output tool but non-json outputMode'.\n \u2022 Build from valid base template; set outputMode: 'last_message' and include 'set_output'. Expect failure and verify message includes ''set_output' tool requires outputMode to be 'json''.\n - Add test: 'should reject template with set_output tool and all_messages outputMode'.\n \u2022 Same as above but with outputMode: 'all_messages'. Expect failure.\n - Add test: 'should reject template with non-empty subagents but missing spawn_agents tool'.\n \u2022 Set subagents to a non-empty array and toolNames not including 'spawn_agents'. Expect failure and message to include: 'Non-empty subagents array requires the 'spawn_agents' tool'.\n - Add test: 'should accept template with non-empty subagents and spawn_agents tool'.\n \u2022 Provide subagents and include 'spawn_agents' in toolNames. Expect success.\n - Add test: 'should accept template with empty subagents and no spawn_agents tool'.\n \u2022 Provide empty subagents and omit 'spawn_agents'. Expect success.\n\nBehavioral expectations:\n- Any template that includes 'set_output' must use outputMode 'json'.\n- Any template with one or more subagents must include the 'spawn_agents' tool.\n- Existing constraints (e.g., json mode requires set_output and outputSchema => json) remain enforced.\n- Error messages must match the specified strings so tests can assert on them.\n", "prompt": "Strengthen dynamic agent template validation so tool usage and output modes are consistent. Specifically, enforce that structured output mode is the only configuration allowed when an agent intends to set a JSON result, and require the agent-spawning tool whenever templates declare subagents. Add thorough unit tests that cover rejection cases for mismatched modes and missing tools, as well as acceptance cases when constraints are satisfied.", "supplementalFiles": [ "common/src/tools/constants.ts", @@ -2940,7 +2937,7 @@ { "path": "common/src/util/agent-template-validation.ts", "status": "modified", - "diff": "Index: common/src/util/agent-template-validation.ts\n===================================================================\n--- common/src/util/agent-template-validation.ts\t699554c (parent)\n+++ common/src/util/agent-template-validation.ts\tbb61b28 (commit)\n@@ -1,47 +1,29 @@\n-import { normalizeAgentNames } from './agent-name-normalization'\n-import { DynamicAgentTemplateSchema } from '../types/dynamic-agent-template'\n import { AgentTemplateTypes } from '../types/session-state'\n \n-import type { AgentOverrideConfig } from '../types/agent-overrides'\n-import type { DynamicAgentTemplate } from '../types/dynamic-agent-template'\n-\n export interface SubagentValidationResult {\n valid: boolean\n invalidAgents: string[]\n }\n \n-export interface AgentTemplateValidationResult {\n- validConfigs: Array<{\n- filePath: string\n- config: AgentOverrideConfig | DynamicAgentTemplate\n- }>\n- validationErrors: Array<{ filePath: string; message: string }>\n-}\n-\n /**\n * Centralized validation for spawnable agents.\n * Validates that all spawnable agents reference valid agent types.\n */\n export function validateSubagents(\n subagents: string[],\n dynamicAgentIds: string[],\n ): SubagentValidationResult & { availableAgents: string[] } {\n- // Normalize dynamic agent IDs to allow users to reference them without org prefixes\n- const normalizedDynamicAgentIds = normalizeAgentNames(dynamicAgentIds)\n \n // Build complete list of available agent types (normalized)\n const availableAgentTypes = [\n ...Object.values(AgentTemplateTypes),\n- ...normalizedDynamicAgentIds,\n+ ...dynamicAgentIds,\n ]\n \n- // Normalize subagents for comparison\n- const normalizedSubagents = normalizeAgentNames(subagents)\n-\n // Find invalid agents (those not in available types after normalization)\n const invalidAgents = subagents.filter(\n- (agent, index) => !availableAgentTypes.includes(normalizedSubagents[index]),\n+ (agent, index) => !availableAgentTypes.includes(subagents[index]),\n )\n \n return {\n valid: invalidAgents.length === 0,\n@@ -50,46 +32,8 @@\n }\n }\n \n /**\n- * Centralized validation for parent instructions.\n- * Validates that all parent instruction keys reference valid agent types.\n- */\n-export function validateParentInstructions(\n- parentInstructions: Record,\n- dynamicAgentIds: string[],\n-): SubagentValidationResult & { availableAgents: string[] } {\n- // Normalize dynamic agent IDs to allow users to reference them without org prefixes\n- const normalizedDynamicAgentIds = normalizeAgentNames(dynamicAgentIds)\n-\n- // Build complete list of available agent types (normalized)\n- const availableAgentTypes = [\n- ...Object.values(AgentTemplateTypes),\n- ...normalizedDynamicAgentIds,\n- ]\n-\n- // Get the keys (agent IDs) from parentInstructions\n- const parentInstructionKeys = Object.keys(parentInstructions)\n-\n- // Normalize parent instruction keys for comparison\n- const normalizedParentInstructionKeys = normalizeAgentNames(\n- parentInstructionKeys,\n- )\n-\n- // Find invalid agents (those not in available types after normalization)\n- const invalidAgents = parentInstructionKeys.filter(\n- (agent, index) =>\n- !availableAgentTypes.includes(normalizedParentInstructionKeys[index]),\n- )\n-\n- return {\n- valid: invalidAgents.length === 0,\n- invalidAgents,\n- availableAgents: availableAgentTypes,\n- }\n-}\n-\n-/**\n * Formats a validation error message for subagents\n */\n export function formatSubagentError(\n invalidAgents: string[],\n@@ -102,22 +46,8 @@\n return message\n }\n \n /**\n- * Formats a validation error message for parent instructions\n- */\n-export function formatParentInstructionsError(\n- invalidAgents: string[],\n- availableAgents: string[],\n-): string {\n- let message = `Invalid parent instruction agent IDs: ${invalidAgents.join(', ')}. Double check the id, including the org prefix if applicable.`\n-\n- message += `\\n\\nAvailable agents: ${availableAgents.join(', ')}`\n-\n- return message\n-}\n-\n-/**\n * Formats validation errors into a user-friendly error message\n * @param validationErrors - Array of validation errors\n * @returns Formatted error message string or undefined if no errors\n */\n@@ -129,83 +59,4 @@\n return validationErrors\n .map((error) => `❌ ${error.filePath}: ${error.message}`)\n .join('\\n')\n }\n-\n-/**\n- * Validates agent template files and returns both valid configs and validation errors\n- * @param agentTemplates - Record of file paths to file contents\n- * @param dynamicAgentIds - Array of dynamic agent IDs to include in validation\n- * @returns Object containing valid configs and validation errors\n- */\n-export function validateAgentTemplateConfigs(\n- agentTemplates: Record,\n- dynamicAgentIds: string[] = [],\n-): AgentTemplateValidationResult {\n- const validConfigs: Array<{\n- filePath: string\n- config: AgentOverrideConfig | DynamicAgentTemplate\n- }> = []\n- const validationErrors: Array<{ filePath: string; message: string }> = []\n-\n- for (const [agentId, content] of Object.entries(agentTemplates)) {\n- try {\n- const config = DynamicAgentTemplateSchema.parse(content)\n-\n- // Additional validation for subagents\n- if (config.subagents && config.subagents.length > 0) {\n- const validation = validateSubagents(config.subagents, dynamicAgentIds)\n- if (!validation.valid) {\n- validationErrors.push({\n- filePath: agentId,\n- message: formatSubagentError(\n- validation.invalidAgents,\n- validation.availableAgents,\n- ),\n- })\n- continue\n- }\n- }\n-\n- validConfigs.push({ filePath: agentId, config })\n- } catch (error) {\n- validationErrors.push({\n- filePath: agentId,\n- message: `Invalid JSON or schema: ${error instanceof Error ? error.message : 'Unknown error'}`,\n- })\n- }\n- }\n-\n- return { validConfigs, validationErrors }\n-}\n-\n-/**\n- * Validates agent template override files and returns only valid ones\n- */\n-export function validateAgentTemplateFiles(\n- agentTemplates: Record,\n- logger?: { warn: (obj: any, msg: string) => void },\n-): Record {\n- const validatedAgents: Record = {}\n- const { validConfigs, validationErrors } =\n- validateAgentTemplateConfigs(agentTemplates)\n-\n- // Add valid configs to validated files\n- for (const { filePath } of validConfigs) {\n- validatedAgents[filePath] = agentTemplates[filePath]\n- }\n-\n- // Log validation errors\n- for (const { filePath, message } of validationErrors) {\n- logger?.warn({ filePath }, message) ??\n- console.warn(`${message}: ${filePath}`)\n- }\n-\n- // Add non-JSON files without validation\n- for (const [filePath, content] of Object.entries(agentTemplates)) {\n- if (!filePath.endsWith('.json')) {\n- validatedAgents[filePath] = content\n- }\n- }\n-\n- return validatedAgents\n-}\n" + "diff": "Index: common/src/util/agent-template-validation.ts\n===================================================================\n--- common/src/util/agent-template-validation.ts\t699554c (parent)\n+++ common/src/util/agent-template-validation.ts\tbb61b28 (commit)\n@@ -1,47 +1,29 @@\n-import { normalizeAgentNames } from './agent-name-normalization'\n-import { DynamicAgentTemplateSchema } from '../types/dynamic-agent-template'\n import { AgentTemplateTypes } from '../types/session-state'\n \n-import type { AgentOverrideConfig } from '../types/agent-overrides'\n-import type { DynamicAgentTemplate } from '../types/dynamic-agent-template'\n-\n export interface SubagentValidationResult {\n valid: boolean\n invalidAgents: string[]\n }\n \n-export interface AgentTemplateValidationResult {\n- validConfigs: Array<{\n- filePath: string\n- config: AgentOverrideConfig | DynamicAgentTemplate\n- }>\n- validationErrors: Array<{ filePath: string; message: string }>\n-}\n-\n /**\n * Centralized validation for spawnable agents.\n * Validates that all spawnable agents reference valid agent types.\n */\n export function validateSubagents(\n subagents: string[],\n dynamicAgentIds: string[],\n ): SubagentValidationResult & { availableAgents: string[] } {\n- // Normalize dynamic agent IDs to allow users to reference them without org prefixes\n- const normalizedDynamicAgentIds = normalizeAgentNames(dynamicAgentIds)\n \n // Build complete list of available agent types (normalized)\n const availableAgentTypes = [\n ...Object.values(AgentTemplateTypes),\n- ...normalizedDynamicAgentIds,\n+ ...dynamicAgentIds,\n ]\n \n- // Normalize subagents for comparison\n- const normalizedSubagents = normalizeAgentNames(subagents)\n-\n // Find invalid agents (those not in available types after normalization)\n const invalidAgents = subagents.filter(\n- (agent, index) => !availableAgentTypes.includes(normalizedSubagents[index]),\n+ (agent, index) => !availableAgentTypes.includes(subagents[index]),\n )\n \n return {\n valid: invalidAgents.length === 0,\n@@ -50,46 +32,8 @@\n }\n }\n \n /**\n- * Centralized validation for parent instructions.\n- * Validates that all parent instruction keys reference valid agent types.\n- */\n-export function validateParentInstructions(\n- parentInstructions: Record,\n- dynamicAgentIds: string[],\n-): SubagentValidationResult & { availableAgents: string[] } {\n- // Normalize dynamic agent IDs to allow users to reference them without org prefixes\n- const normalizedDynamicAgentIds = normalizeAgentNames(dynamicAgentIds)\n-\n- // Build complete list of available agent types (normalized)\n- const availableAgentTypes = [\n- ...Object.values(AgentTemplateTypes),\n- ...normalizedDynamicAgentIds,\n- ]\n-\n- // Get the keys (agent IDs) from parentInstructions\n- const parentInstructionKeys = Object.keys(parentInstructions)\n-\n- // Normalize parent instruction keys for comparison\n- const normalizedParentInstructionKeys = normalizeAgentNames(\n- parentInstructionKeys,\n- )\n-\n- // Find invalid agents (those not in available types after normalization)\n- const invalidAgents = parentInstructionKeys.filter(\n- (agent, index) =>\n- !availableAgentTypes.includes(normalizedParentInstructionKeys[index]),\n- )\n-\n- return {\n- valid: invalidAgents.length === 0,\n- invalidAgents,\n- availableAgents: availableAgentTypes,\n- }\n-}\n-\n-/**\n * Formats a validation error message for subagents\n */\n export function formatSubagentError(\n invalidAgents: string[],\n@@ -102,22 +46,8 @@\n return message\n }\n \n /**\n- * Formats a validation error message for parent instructions\n- */\n-export function formatParentInstructionsError(\n- invalidAgents: string[],\n- availableAgents: string[],\n-): string {\n- let message = `Invalid parent instruction agent IDs: ${invalidAgents.join(', ')}. Double check the id, including the org prefix if applicable.`\n-\n- message += `\\n\\nAvailable agents: ${availableAgents.join(', ')}`\n-\n- return message\n-}\n-\n-/**\n * Formats validation errors into a user-friendly error message\n * @param validationErrors - Array of validation errors\n * @returns Formatted error message string or undefined if no errors\n */\n@@ -129,83 +59,4 @@\n return validationErrors\n .map((error) => `\u274c ${error.filePath}: ${error.message}`)\n .join('\\n')\n }\n-\n-/**\n- * Validates agent template files and returns both valid configs and validation errors\n- * @param agentTemplates - Record of file paths to file contents\n- * @param dynamicAgentIds - Array of dynamic agent IDs to include in validation\n- * @returns Object containing valid configs and validation errors\n- */\n-export function validateAgentTemplateConfigs(\n- agentTemplates: Record,\n- dynamicAgentIds: string[] = [],\n-): AgentTemplateValidationResult {\n- const validConfigs: Array<{\n- filePath: string\n- config: AgentOverrideConfig | DynamicAgentTemplate\n- }> = []\n- const validationErrors: Array<{ filePath: string; message: string }> = []\n-\n- for (const [agentId, content] of Object.entries(agentTemplates)) {\n- try {\n- const config = DynamicAgentTemplateSchema.parse(content)\n-\n- // Additional validation for subagents\n- if (config.subagents && config.subagents.length > 0) {\n- const validation = validateSubagents(config.subagents, dynamicAgentIds)\n- if (!validation.valid) {\n- validationErrors.push({\n- filePath: agentId,\n- message: formatSubagentError(\n- validation.invalidAgents,\n- validation.availableAgents,\n- ),\n- })\n- continue\n- }\n- }\n-\n- validConfigs.push({ filePath: agentId, config })\n- } catch (error) {\n- validationErrors.push({\n- filePath: agentId,\n- message: `Invalid JSON or schema: ${error instanceof Error ? error.message : 'Unknown error'}`,\n- })\n- }\n- }\n-\n- return { validConfigs, validationErrors }\n-}\n-\n-/**\n- * Validates agent template override files and returns only valid ones\n- */\n-export function validateAgentTemplateFiles(\n- agentTemplates: Record,\n- logger?: { warn: (obj: any, msg: string) => void },\n-): Record {\n- const validatedAgents: Record = {}\n- const { validConfigs, validationErrors } =\n- validateAgentTemplateConfigs(agentTemplates)\n-\n- // Add valid configs to validated files\n- for (const { filePath } of validConfigs) {\n- validatedAgents[filePath] = agentTemplates[filePath]\n- }\n-\n- // Log validation errors\n- for (const { filePath, message } of validationErrors) {\n- logger?.warn({ filePath }, message) ??\n- console.warn(`${message}: ${filePath}`)\n- }\n-\n- // Add non-JSON files without validation\n- for (const [filePath, content] of Object.entries(agentTemplates)) {\n- if (!filePath.endsWith('.json')) {\n- validatedAgents[filePath] = content\n- }\n- }\n-\n- return validatedAgents\n-}\n" }, { "path": "web/src/components/docs/mdx/mdx-components.tsx", @@ -2960,7 +2957,7 @@ { "path": "web/src/content/agents/troubleshooting-agent-customization.mdx", "status": "modified", - "diff": "Index: web/src/content/agents/troubleshooting-agent-customization.mdx\n===================================================================\n--- web/src/content/agents/troubleshooting-agent-customization.mdx\t699554c (parent)\n+++ web/src/content/agents/troubleshooting-agent-customization.mdx\tbb61b28 (commit)\n@@ -224,9 +224,8 @@\n \n ```markdown\n your-project/\n ├── .agents/\n-│ └── templates/\n │ ├── my-agent.json\n │ └── my-prompts.md\n ```\n \n" + "diff": "Index: web/src/content/agents/troubleshooting-agent-customization.mdx\n===================================================================\n--- web/src/content/agents/troubleshooting-agent-customization.mdx\t699554c (parent)\n+++ web/src/content/agents/troubleshooting-agent-customization.mdx\tbb61b28 (commit)\n@@ -224,9 +224,8 @@\n \n ```markdown\n your-project/\n \u251c\u2500\u2500 .agents/\n-\u2502 \u2514\u2500\u2500 templates/\n \u2502 \u251c\u2500\u2500 my-agent.json\n \u2502 \u2514\u2500\u2500 my-prompts.md\n ```\n \n" } ] }, @@ -2968,8 +2965,8 @@ "id": "simplify-tool-result", "sha": "9bd3253ae89b60f8362e30531d710f7d984cf418", "parentSha": "e24b851c02ff435aad0078e3ab69954c2e090bf2", - "spec": "Implement a migration so programmatic agent handleSteps generators receive only the latest tool result content as a string (or undefined), not the ToolResult wrapper object. Apply the following changes:\n\n1) Type updates (generator contract)\n- common/src/types/agent-template.ts: Update StepGenerator’s third generic parameter to be { agentState: AgentState; toolResult: string | undefined } instead of ToolResult | undefined.\n- .agents/types/agent-config.d.ts: Mirror the same change for the programmatic agent template types. Update the inline usage docs/examples to no longer inspect thinkResult.toolName; instead, simply yield 'STEP' (end-turn detection is handled elsewhere).\n\n2) Programmatic step runner behavior\n- backend/src/run-programmatic-step.ts: When resuming the generator after a tool call, pass only the latest tool result string (toolResults[toolResults.length - 1]?.result) as toolResult. Maintain end-turn detection by checking the yielded tool call name (e.g., if toolName === 'end_turn', set endTurn and break) rather than inspecting the prior wrapper passed into the generator.\n\n3) Update programmatic agents/templates to consume string results\n- backend/src/templates/agents/file-explorer.ts: After spawn_agents, treat the yielded spawnResult as a string and feed it directly into set_output args.results (remove .result usage).\n- backend/src/templates/agents/thinking-base.ts: Remove reliance on toolResult wrapper fields (e.g., thinkResult?.toolName). Do not break on end-turn via toolResult; just yield 'STEP'.\n- .agents/sonnet4-agent-builder.ts: Treat outputs from read_docs/read_files style tools as strings. When writing files, pass the string result directly in args.content. Where exampleAgentsResult was previously exampleAgentsResult?.result, use the string directly and split as needed.\n\n4) Update agent implementation details in researcher\n- .agents/researcher.ts: Ensure web_search is called with a safe default query (prompt ?? '') and set depth to 'standard'.\n\n5) Tests\n- backend/src/__tests__/run-programmatic-step.test.ts: Update expectations to treat tool results passed back to the generator as strings (e.g., expect(receivedToolResult).toEqual('file content') and substring checks like toContain('authenticate')). Remove assertions that inspect wrapper fields (toolName/result) on the generator-provided toolResult.\n\n6) Preserve ToolResult usage elsewhere\n- Do not change ToolResult type or its usage in the broader tool pipeline (tool-executor, stream-parser, run-agent-step, message rendering). Tool execution should continue to accumulate ToolResult[] for state, traces, and message rendering; only the generator handback switches to string.\n\nAcceptance criteria:\n- All type checks pass with the new generator input type.\n- Programmatic agents correctly receive string results and no longer reference wrapper fields.\n- Tests expecting string tool results in the generator pass, including comprehensive STEP/STEP_ALL flows.\n- Researcher agent safely handles empty prompts and uses standard depth.\n- Existing tool execution and message rendering behavior remains unchanged outside the generator input contract.", - "prompt": "Refactor programmatic agent step handling so that generators receive only the latest tool’s result text. Update the types, the step runner to pass a string or undefined, and all affected agent templates and tests that previously accessed wrapper fields. Keep the broader tool execution pipeline unchanged. Also make the researcher agent’s web search safer by defaulting the query and using a standard depth.", + "spec": "Implement a migration so programmatic agent handleSteps generators receive only the latest tool result content as a string (or undefined), not the ToolResult wrapper object. Apply the following changes:\n\n1) Type updates (generator contract)\n- common/src/types/agent-template.ts: Update StepGenerator\u2019s third generic parameter to be { agentState: AgentState; toolResult: string | undefined } instead of ToolResult | undefined.\n- .agents/types/agent-config.d.ts: Mirror the same change for the programmatic agent template types. Update the inline usage docs/examples to no longer inspect thinkResult.toolName; instead, simply yield 'STEP' (end-turn detection is handled elsewhere).\n\n2) Programmatic step runner behavior\n- backend/src/run-programmatic-step.ts: When resuming the generator after a tool call, pass only the latest tool result string (toolResults[toolResults.length - 1]?.result) as toolResult. Maintain end-turn detection by checking the yielded tool call name (e.g., if toolName === 'end_turn', set endTurn and break) rather than inspecting the prior wrapper passed into the generator.\n\n3) Update programmatic agents/templates to consume string results\n- backend/src/templates/agents/file-explorer.ts: After spawn_agents, treat the yielded spawnResult as a string and feed it directly into set_output args.results (remove .result usage).\n- backend/src/templates/agents/thinking-base.ts: Remove reliance on toolResult wrapper fields (e.g., thinkResult?.toolName). Do not break on end-turn via toolResult; just yield 'STEP'.\n- .agents/sonnet4-agent-builder.ts: Treat outputs from read_docs/read_files style tools as strings. When writing files, pass the string result directly in args.content. Where exampleAgentsResult was previously exampleAgentsResult?.result, use the string directly and split as needed.\n\n4) Update agent implementation details in researcher\n- .agents/researcher.ts: Ensure web_search is called with a safe default query (prompt ?? '') and set depth to 'standard'.\n\n5) Tests\n- backend/src/__tests__/run-programmatic-step.test.ts: Update expectations to treat tool results passed back to the generator as strings (e.g., expect(receivedToolResult).toEqual('file content') and substring checks like toContain('authenticate')). Remove assertions that inspect wrapper fields (toolName/result) on the generator-provided toolResult.\n\n6) Preserve ToolResult usage elsewhere\n- Do not change ToolResult type or its usage in the broader tool pipeline (tool-executor, stream-parser, run-agent-step, message rendering). Tool execution should continue to accumulate ToolResult[] for state, traces, and message rendering; only the generator handback switches to string.\n\nAcceptance criteria:\n- All type checks pass with the new generator input type.\n- Programmatic agents correctly receive string results and no longer reference wrapper fields.\n- Tests expecting string tool results in the generator pass, including comprehensive STEP/STEP_ALL flows.\n- Researcher agent safely handles empty prompts and uses standard depth.\n- Existing tool execution and message rendering behavior remains unchanged outside the generator input contract.", + "prompt": "Refactor programmatic agent step handling so that generators receive only the latest tool\u2019s result text. Update the types, the step runner to pass a string or undefined, and all affected agent templates and tests that previously accessed wrapper fields. Keep the broader tool execution pipeline unchanged. Also make the researcher agent\u2019s web search safer by defaulting the query and using a standard depth.", "supplementalFiles": [ "backend/src/tools/tool-executor.ts", "backend/src/tools/stream-parser.ts", @@ -3036,7 +3033,7 @@ "sha": "e24b851c02ff435aad0078e3ab69954c2e090bf2", "parentSha": "3fe0550b5a804d5b28b731a115b827bf93b68aa5", "spec": "Implement an open-source-only agent suite and model explicitness-based routing/caching.\n\n1) Add new agent configs (TypeScript, no code generation here) under .agents/opensource/ using AgentConfig from ../types/agent-config:\n- .agents/opensource/base.ts\n - id: 'oss-model-base'; publisher: 'codebuff'; model: 'qwen/qwen3-235b-a22b-2507:fast'\n - displayName: 'Buffy the Coding Assistant'\n - parentPrompt: Base orchestration description (reliable coding assistance with strong tool use)\n - inputSchema: { prompt: string }\n - outputMode: 'last_message'; includeMessageHistory: false\n - toolNames: ['create_plan','spawn_agents','add_subgoal','browser_logs','end_turn','read_files','think_deeply','run_terminal_command','update_subgoal']\n - subagents: ['codebuff/oss-model-file-picker@0.0.1','codebuff/oss-model-researcher@0.0.1','codebuff/oss-model-thinker@0.0.1','codebuff/oss-model-reviewer@0.0.1','codebuff/oss-model-coder@0.0.1']\n - systemPrompt: Persona and tool/agents/file tree placeholders ({CODEBUFF_*}) matching the diff content\n - instructionsPrompt: Orchestration-only; always delegate code changes to 'oss-model-coder'; list delegation strategy per subagent\n - stepPrompt: \"Continue working on the user's request. Use your tools and spawn subagents as needed.\"\n\n- .agents/opensource/coder.ts\n - id: 'oss-model-coder'; model: 'qwen/qwen3-coder:fast'; displayName: 'Casey the Coder'\n - toolNames: ['read_files','write_file','str_replace','code_search','run_terminal_command','end_turn']\n - subagents: []\n - systemPrompt/instructionsPrompt/stepPrompt content aligning with the diff (coding specialist, read before write, minimal focused edits, end with end_turn)\n\n- .agents/opensource/file-picker.ts\n - id: 'oss-model-file-picker'; model: 'openai/gpt-oss-120b:fast'; displayName: 'Fletcher the File Fetcher'\n - toolNames: ['find_files']\n - includeMessageHistory: false; subagents: []\n - systemPrompt/instructionsPrompt/stepPrompt as in diff; add handleSteps generator: first yield find_files with args { prompt: prompt ?? \"Find files related to the user's request\" }, then yield 'STEP_ALL'\n\n- .agents/opensource/researcher.ts\n - id: 'oss-model-researcher'; model: 'qwen/qwen3-235b-a22b-thinking-2507'\n - toolNames: ['web_search','read_docs','read_files','end_turn']\n - systemPrompt/instructionsPrompt/stepPrompt per diff (external research, summarize notes)\n\n- .agents/opensource/reviewer.ts\n - id: 'oss-model-reviewer'; model: 'openai/gpt-oss-120b:fast'; includeMessageHistory: true\n - toolNames: ['end_turn','run_file_change_hooks']\n - systemPrompt/instructionsPrompt/stepPrompt per diff; ensure guidance to run hooks and include results\n\n- .agents/opensource/thinker.ts\n - id: 'oss-model-thinker'; model: 'meta-llama/llama-4-maverick-8b:fast'; includeMessageHistory: true\n - toolNames: ['end_turn']; subagents: []\n - systemPrompt/instructionsPrompt/stepPrompt per diff (concise deep thinking; end with end_turn)\n\n2) Update OpenRouter provider behavior to use model explicitness for fallbacks:\n- Edit backend/src/llm-apis/openrouter.ts\n - Import: isExplicitlyDefinedModel from '@codebuff/common/util/model-utils'\n - Initialize extraBody as a Record; set extraBody.provider = { order: providerOrder[model as keyof typeof providerOrder], allow_fallbacks: !isExplicitlyDefinedModel(model) }\n - Preserve existing providerOrder constants and createOpenRouter call; keep headers unchanged.\n\n3) Add explicit-model utility for shared use:\n- Create common/src/util/model-utils.ts\n - Implement a cached Set of Object.values(models) built via dynamic require('../constants') to avoid circular imports\n - Export function isExplicitlyDefinedModel(model: Model): boolean that checks membership in that Set\n\n4) Update cache-control logic to rely on explicitness:\n- Edit common/src/constants.ts\n - Import isExplicitlyDefinedModel from './util/model-utils'\n - Remove modelsGeneric helper if only used by supportsCacheControl\n - Change supportsCacheControl(model): return false if !isExplicitlyDefinedModel(model); else return !nonCacheableModels.includes(model)\n\nBehavioral expectations:\n- New OSS agents can be referenced by ID and spawned like existing agents, with only open-source model IDs in their configs.\n- OpenRouter requests will allow provider fallbacks for non-explicitly-defined model strings; explicitly-defined models will not allow fallbacks.\n- supportsCacheControl returns true only for explicitly-defined models not in the nonCacheable list (e.g., false for unknown/free-form model IDs).\n- No changes to existing agent files outside the new .agents/opensource suite.", - "prompt": "Add a new suite of open‑source–only agents for orchestration, coding, file discovery, research, review, and deep thinking under a dedicated namespace, using appropriate open‑source model IDs. Update the OpenRouter integration so that provider fallbacks are enabled for non‑explicit model strings but disabled for known, explicitly defined models. Introduce a small shared utility to detect whether a model is explicitly defined and use it to make cache‑control decisions. Keep changes minimal and consistent with existing agent patterns and prompts.", + "prompt": "Add a new suite of open\u2011source\u2013only agents for orchestration, coding, file discovery, research, review, and deep thinking under a dedicated namespace, using appropriate open\u2011source model IDs. Update the OpenRouter integration so that provider fallbacks are enabled for non\u2011explicit model strings but disabled for known, explicitly defined models. Introduce a small shared utility to detect whether a model is explicitly defined and use it to make cache\u2011control decisions. Keep changes minimal and consistent with existing agent patterns and prompts.", "supplementalFiles": [ ".agents/base.ts", ".agents/file-picker.ts", @@ -3102,7 +3099,7 @@ "sha": "aff88fde0167ee6b93f5fd68861f6cc30889d64c", "parentSha": "80017710720bdd0edf24651b2732e410275ef75f", "spec": "- Goal: Migrate agent prompt strings in .agents to multiline template literals and introduce a conversion script to automate future migrations.\n\n- Scope: Update the following files to use template literals (backticks) with actual newlines for prompt fields and normalize minor formatting where applicable:\n - .agents/ask.ts\n - .agents/base-experimental.ts\n - .agents/base-lite.ts\n - .agents/base-max.ts\n - .agents/base.ts\n - .agents/claude4-gemini-thinking.ts\n - .agents/file-picker.ts\n - .agents/knowledge-keeper.ts\n - .agents/planner.ts\n - .agents/researcher.ts\n - .agents/reviewer.ts\n - .agents/sonnet4-agent-builder.ts\n - .agents/superagent.ts\n - .agents/thinker.ts\n\n- Required changes in each agent file:\n 1) For properties systemPrompt, instructionsPrompt, and stepPrompt:\n - Replace single/double-quoted strings containing escaped newlines (\\n) with backtick template literals.\n - Replace all escaped newlines (\\n) with actual newlines.\n - Escape any literal backticks in the content (use \\`).\n - Preserve all existing content, placeholders (e.g., {CODEBUFF_*}), and whitespace semantics.\n 2) Ensure XML/system instruction blocks within prompts are no longer escape-prefixed and are readable as intended (e.g., ..., ...), including properly closed tags.\n 3) Where stepPrompt or systemPrompt previously had slightly malformed delimiters (e.g., extra escapes), normalize them to clean, human-readable blocks without altering meaning.\n 4) Do not alter agent behavior, models, tools, input/output schemas, or handleSteps logic.\n\n- Add a new automation script:\n - Path: scripts/convert-escaped-newlines.ts\n - Behavior:\n - Shebang for Bun (#!/usr/bin/env bun).\n - Scan the .agents directory for .ts files (non-recursive is acceptable for current structure).\n - For each file, identify string-valued properties of the form : '...\\n...' or \"...\\n...\" and only transform those that contain escaped newlines.\n - Transformations per match:\n - Escape any existing backticks in the content.\n - Replace all \\n sequences with actual newlines.\n - Replace the surrounding quotes with backticks.\n - Reconstruct as : `...` while preserving other file content.\n - Log progress (processing, converted properties per file, counts), and write back only if modified.\n - No changes to loaders/validators are required; they already consume string prompts transparently.\n\n- Verification criteria:\n - All listed .agents files use template literals for prompts with readable, multiline content.\n - The prompts render exactly the same semantics as before (no missing placeholders, no malformed XML-like tags, no unintended escapes).\n - The script runs with Bun and reports processed/modified file counts.\n - Existing loaders (npm-app/src/agents/load-agents.ts, backend/src/templates/agent-registry.ts) accept the updated prompts without changes.\n - Unit/integration tests that assert tool-call XML and prompt assembly pass unchanged.", - "prompt": "Refactor all agent prompt strings in the .agents directory to use multiline template literals instead of quoted strings with escaped newlines. Preserve all content and placeholders while making the text human-readable and removing escape sequences. Add a small Bun script under scripts/ that scans .agents and converts any prompt fields containing \\n into template literals, safely escaping backticks and replacing \\n with actual newlines. Do not change agent behavior or loaders—only the prompt string formatting and the new script.", + "prompt": "Refactor all agent prompt strings in the .agents directory to use multiline template literals instead of quoted strings with escaped newlines. Preserve all content and placeholders while making the text human-readable and removing escape sequences. Add a small Bun script under scripts/ that scans .agents and converts any prompt fields containing \\n into template literals, safely escaping backticks and replacing \\n with actual newlines. Do not change agent behavior or loaders\u2014only the prompt string formatting and the new script.", "supplementalFiles": [ "npm-app/src/agents/load-agents.ts", "backend/src/templates/agent-registry.ts", @@ -3188,7 +3185,7 @@ { "path": "scripts/convert-escaped-newlines.ts", "status": "added", - "diff": "Index: scripts/convert-escaped-newlines.ts\n===================================================================\n--- scripts/convert-escaped-newlines.ts\t8001771 (parent)\n+++ scripts/convert-escaped-newlines.ts\taff88fd (commit)\n@@ -1,1 +1,87 @@\n-[NEW FILE]\n\\ No newline at end of file\n+#!/usr/bin/env bun\n+\n+import { readdir, readFile, writeFile } from 'fs/promises'\n+import { join } from 'path'\n+\n+/**\n+ * Script to convert escaped newline strings to template literals in .agents folder\n+ * \n+ * Algorithm:\n+ * 1. Find all TypeScript files in .agents folder\n+ * 2. For each file, find string properties that contain escaped newlines\n+ * 3. Escape any existing backticks in the string content\n+ * 4. Convert the string wrapper from quotes to backticks\n+ * 5. Replace \\n with actual newlines\n+ */\n+\n+async function convertFile(filePath: string): Promise {\n+ console.log(`Processing: ${filePath}`)\n+ \n+ const content = await readFile(filePath, 'utf-8')\n+ let modified = false\n+ \n+ // Pattern to match string properties that contain escaped newlines\n+ // Matches: propertyName: 'string with \\n' or propertyName: \"string with \\n\"\n+ const stringWithNewlinesPattern = /(\\w+):\\s*(['\"])((?:(?!\\2)[^\\\\]|\\\\[\\s\\S])*)\\2/g\n+ \n+ const newContent = content.replace(stringWithNewlinesPattern, (match, propertyName, quote, stringContent) => {\n+ // Only process if the string contains escaped newlines\n+ if (!stringContent.includes('\\\\n')) {\n+ return match\n+ }\n+ \n+ console.log(` Converting property: ${propertyName}`)\n+ modified = true\n+ \n+ // Step 1: Escape any existing backticks in the string content\n+ let processedContent = stringContent.replace(/`/g, '\\\\`')\n+ \n+ // Step 2: Replace escaped newlines with actual newlines\n+ processedContent = processedContent.replace(/\\\\n/g, '\\n')\n+ \n+ // Step 3: Convert to template literal\n+ return `${propertyName}: \\`${processedContent}\\``\n+ })\n+ \n+ if (modified) {\n+ await writeFile(filePath, newContent, 'utf-8')\n+ console.log(` ✅ Updated: ${filePath}`)\n+ return true\n+ } else {\n+ console.log(` ⏭️ No changes needed: ${filePath}`)\n+ return false\n+ }\n+}\n+\n+async function main() {\n+ const agentsDir = '.agents'\n+ \n+ try {\n+ const files = await readdir(agentsDir)\n+ const tsFiles = files.filter(file => file.endsWith('.ts'))\n+ \n+ console.log(`Found ${tsFiles.length} TypeScript files in ${agentsDir}/`)\n+ \n+ let totalModified = 0\n+ \n+ for (const file of tsFiles) {\n+ const filePath = join(agentsDir, file)\n+ const wasModified = await convertFile(filePath)\n+ if (wasModified) {\n+ totalModified++\n+ }\n+ }\n+ \n+ console.log(`\\n🎉 Conversion complete!`)\n+ console.log(`📊 Files processed: ${tsFiles.length}`)\n+ console.log(`✏️ Files modified: ${totalModified}`)\n+ \n+ } catch (error) {\n+ console.error('Error:', error)\n+ process.exit(1)\n+ }\n+}\n+\n+if (import.meta.main) {\n+ main()\n+}\n" + "diff": "Index: scripts/convert-escaped-newlines.ts\n===================================================================\n--- scripts/convert-escaped-newlines.ts\t8001771 (parent)\n+++ scripts/convert-escaped-newlines.ts\taff88fd (commit)\n@@ -1,1 +1,87 @@\n-[NEW FILE]\n\\ No newline at end of file\n+#!/usr/bin/env bun\n+\n+import { readdir, readFile, writeFile } from 'fs/promises'\n+import { join } from 'path'\n+\n+/**\n+ * Script to convert escaped newline strings to template literals in .agents folder\n+ * \n+ * Algorithm:\n+ * 1. Find all TypeScript files in .agents folder\n+ * 2. For each file, find string properties that contain escaped newlines\n+ * 3. Escape any existing backticks in the string content\n+ * 4. Convert the string wrapper from quotes to backticks\n+ * 5. Replace \\n with actual newlines\n+ */\n+\n+async function convertFile(filePath: string): Promise {\n+ console.log(`Processing: ${filePath}`)\n+ \n+ const content = await readFile(filePath, 'utf-8')\n+ let modified = false\n+ \n+ // Pattern to match string properties that contain escaped newlines\n+ // Matches: propertyName: 'string with \\n' or propertyName: \"string with \\n\"\n+ const stringWithNewlinesPattern = /(\\w+):\\s*(['\"])((?:(?!\\2)[^\\\\]|\\\\[\\s\\S])*)\\2/g\n+ \n+ const newContent = content.replace(stringWithNewlinesPattern, (match, propertyName, quote, stringContent) => {\n+ // Only process if the string contains escaped newlines\n+ if (!stringContent.includes('\\\\n')) {\n+ return match\n+ }\n+ \n+ console.log(` Converting property: ${propertyName}`)\n+ modified = true\n+ \n+ // Step 1: Escape any existing backticks in the string content\n+ let processedContent = stringContent.replace(/`/g, '\\\\`')\n+ \n+ // Step 2: Replace escaped newlines with actual newlines\n+ processedContent = processedContent.replace(/\\\\n/g, '\\n')\n+ \n+ // Step 3: Convert to template literal\n+ return `${propertyName}: \\`${processedContent}\\``\n+ })\n+ \n+ if (modified) {\n+ await writeFile(filePath, newContent, 'utf-8')\n+ console.log(` \u2705 Updated: ${filePath}`)\n+ return true\n+ } else {\n+ console.log(` \u23ed\ufe0f No changes needed: ${filePath}`)\n+ return false\n+ }\n+}\n+\n+async function main() {\n+ const agentsDir = '.agents'\n+ \n+ try {\n+ const files = await readdir(agentsDir)\n+ const tsFiles = files.filter(file => file.endsWith('.ts'))\n+ \n+ console.log(`Found ${tsFiles.length} TypeScript files in ${agentsDir}/`)\n+ \n+ let totalModified = 0\n+ \n+ for (const file of tsFiles) {\n+ const filePath = join(agentsDir, file)\n+ const wasModified = await convertFile(filePath)\n+ if (wasModified) {\n+ totalModified++\n+ }\n+ }\n+ \n+ console.log(`\\n\ud83c\udf89 Conversion complete!`)\n+ console.log(`\ud83d\udcca Files processed: ${tsFiles.length}`)\n+ console.log(`\u270f\ufe0f Files modified: ${totalModified}`)\n+ \n+ } catch (error) {\n+ console.error('Error:', error)\n+ process.exit(1)\n+ }\n+}\n+\n+if (import.meta.main) {\n+ main()\n+}\n" } ] } diff --git a/evals/buffbench/gen-evals.ts b/evals/buffbench/gen-evals.ts index db9a8177e5..eb07704d10 100644 --- a/evals/buffbench/gen-evals.ts +++ b/evals/buffbench/gen-evals.ts @@ -5,9 +5,8 @@ import path from 'path' import { mapLimit } from 'async' import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants' -import { getUserCredentials } from '@codebuff/npm-app/credentials' -import { CodebuffClient } from '@codebuff/sdk' +import { CodebuffClient, getUserCredentials } from '@codebuff/sdk' import { extractRepoNameFromUrl } from './setup-test-repo' import { withTestRepoAndParent } from '../subagents/test-repo-utils' import { generateEvalTask } from './eval-task-generator' diff --git a/evals/buffbench/pick-commits.ts b/evals/buffbench/pick-commits.ts index 29d4c6abe4..2d1855ea63 100644 --- a/evals/buffbench/pick-commits.ts +++ b/evals/buffbench/pick-commits.ts @@ -5,9 +5,9 @@ import fs from 'fs' 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 { models } from '@codebuff/common/old-constants' import { userMessage } from '@codebuff/common/util/messages' +import { promptAiSdkStructured } from '@codebuff/sdk' import { mapLimit } from 'async' import { z } from 'zod/v4' diff --git a/evals/buffbench/run-buffbench.ts b/evals/buffbench/run-buffbench.ts index 39bb9f5361..7be48bd30d 100644 --- a/evals/buffbench/run-buffbench.ts +++ b/evals/buffbench/run-buffbench.ts @@ -1,23 +1,26 @@ +import { execSync } from 'child_process' import fs from 'fs' -import path from 'path' import os from 'os' -import { execSync } from 'child_process' +import path from 'path' import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants' -import { getUserCredentials } from '@codebuff/npm-app/credentials' -import { loadLocalAgents } from '@codebuff/npm-app/agents/load-agents' +import { + CodebuffClient, + getUserCredentials, + loadLocalAgents, +} from '@codebuff/sdk' import pLimit from 'p-limit' import { runAgentOnCommit, type ExternalAgentType } from './agent-runner' import { formatTaskResults } from './format-output' import { judgeCommitResult } from './judge' -import { analyzeAgentTraces, type AgentTraceData } from './trace-analyzer' import { extractAgentLessons, saveAgentLessons } from './lessons-extractor' -import { CodebuffClient } from '@codebuff/sdk' +import { analyzeAgentTraces, type AgentTraceData } from './trace-analyzer' import { logger } from '../logger' -import type { AgentEvalResults, EvalDataV2, EvalCommitV2 } from './types' import { analyzeAllTasks } from './meta-analyzer' +import type { AgentEvalResults, EvalDataV2, EvalCommitV2 } from './types' + function parseAgentId(agent: string): { agentId: string externalAgentType?: ExternalAgentType @@ -342,12 +345,12 @@ export async function runBuffBench(options: { (f) => f.data.binInstalls ?? [], ) const uniqueBinInstalls = allBinInstalls.filter( - (bin, index, self) => - index === self.findIndex((b) => b.name === bin.name), + (bin, index, self) => index === self.findIndex((b) => b.name === bin.name), ) // Install binaries once at the beginning - const { tempDir: binsTempDir, env: binsEnv } = installBinaries(uniqueBinInstalls) + const { tempDir: binsTempDir, env: binsEnv } = + installBinaries(uniqueBinInstalls) let commitsToRun: CommitWithSource[] if (taskIds && taskIds.length > 0) { @@ -364,7 +367,9 @@ export async function runBuffBench(options: { } if (notFoundIds.length > 0) { - const availableIds = allCommitsWithSource.map((c) => c.commit.id).join(', ') + const availableIds = allCommitsWithSource + .map((c) => c.commit.id) + .join(', ') throw new Error( `Task ID(s) not found: ${notFoundIds.join(', ')}. Available task IDs: ${availableIds}`, ) @@ -475,7 +480,7 @@ export async function runBuffBench(options: { } } - for (const [_agentId, agentData] of Object.entries(results)) { + for (const agentData of Object.values(results)) { // Filter out runs from commits where ANY agent had an error const validRuns = agentData.runs.filter( (r) => !commitShasWithErrors.has(r.commitSha), diff --git a/evals/package.json b/evals/package.json index 25bfd2ba1a..f5b99c5574 100644 --- a/evals/package.json +++ b/evals/package.json @@ -31,11 +31,9 @@ }, "dependencies": { "@anthropic-ai/claude-code": "^2.0.56", - "@codebuff/backend": "workspace:*", "@codebuff/code-map": "workspace:*", "@codebuff/common": "workspace:*", "@codebuff/internal": "workspace:*", - "@codebuff/npm-app": "workspace:*", "@codebuff/sdk": "workspace:*", "@oclif/core": "^4.4.0", "@oclif/parser": "^3.8.17", diff --git a/evals/scaffolding.ts b/evals/scaffolding.ts index ecbbb0a095..ae80466d71 100644 --- a/evals/scaffolding.ts +++ b/evals/scaffolding.ts @@ -1,26 +1,15 @@ import { execSync } from 'child_process' -import { EventEmitter } from 'events' import fs from 'fs' import path from 'path' import { runAgentStep } from '@codebuff/agent-runtime/run-agent-step' import { assembleLocalAgentTemplates } from '@codebuff/agent-runtime/templates/agent-registry' -import { - handleStepsLogChunkWs, - requestFilesWs, - requestMcpToolDataWs, - requestOptionalFileWs, - requestToolCallWs, - sendActionWs, - sendSubagentChunkWs, -} from '@codebuff/backend/client-wrapper' import { getFileTokenScores } from '@codebuff/code-map/parse' import { API_KEY_ENV_VAR, TEST_USER_ID } from '@codebuff/common/old-constants' -import { mockModule } from '@codebuff/common/testing/mock-modules' +import { clientToolCallSchema } from '@codebuff/common/tools/list' import { generateCompactId } from '@codebuff/common/util/string' -import { handleToolCall } from '@codebuff/npm-app/tool-handlers' -import { getSystemInfo } from '@codebuff/npm-app/utils/system-info' -import { mock } from 'bun:test' +import { getSystemInfo } from '@codebuff/common/util/system-info' +import { ToolHelpers } from '@codebuff/sdk' import { blue } from 'picocolors' import { EVALS_AGENT_RUNTIME_IMPL } from './impl/agent-runtime' @@ -31,6 +20,7 @@ import { import type { ClientToolCall } from '@codebuff/common/tools/list' import type { AgentRuntimeScopedDeps } from '@codebuff/common/types/contracts/agent-runtime' +import type { CodebuffFileSystem } from '@codebuff/common/types/filesystem' import type { ToolMessage } from '@codebuff/common/types/messages/codebuff-message' import type { ToolResultOutput } from '@codebuff/common/types/messages/content-part' import type { PrintModeEvent } from '@codebuff/common/types/print-mode' @@ -40,7 +30,6 @@ import type { SessionState, } from '@codebuff/common/types/session-state' import type { ProjectFileContext } from '@codebuff/common/util/file' -import type { WebSocket } from 'ws' const DEBUG_MODE = true @@ -55,67 +44,116 @@ function readMockFile(projectRoot: string, filePath: string): string | null { let toolCalls: ClientToolCall[] = [] let toolResults: ToolMessage[] = [] -export async function createFileReadingMock(projectRoot: string) { - await mockModule('@codebuff/backend/websockets/websocket-action', () => ({ - requestFiles: ((params: { ws: WebSocket; filePaths: string[] }) => { - const files: Record = {} - for (const filePath of params.filePaths) { - files[filePath] = readMockFile(projectRoot, filePath) - } - return Promise.resolve(files) - }) satisfies typeof requestFilesWs, - requestToolCall: (async (params: { - ws: WebSocket - userInputId: string - toolName: string - input: Record - }): ReturnType => { - const { toolName, input } = params - // Execute the tool call using existing tool handlers - const toolCall = { - toolCallId: generateCompactId(), - toolName, - input, - } - toolCalls.push(toolCall as ClientToolCall) - try { - const toolResult = await handleToolCall(toolCall as any) - toolResults.push({ - role: 'tool', - toolName: toolCall.toolName, - toolCallId: toolCall.toolCallId, - content: toolResult.content, - }) +const defaultFs: CodebuffFileSystem = { + ...(fs.promises as unknown as CodebuffFileSystem), + exists: async (pathLike) => { + try { + await fs.promises.access(pathLike as fs.PathLike) + return true + } catch { + return false + } + }, +} +let projectRootForMocks: string | undefined + +export function createFileReadingMock(projectRoot: string) { + projectRootForMocks = projectRoot +} + +function getActiveProjectRoot(fileContext?: ProjectFileContext) { + return fileContext?.projectRoot ?? projectRootForMocks ?? process.cwd() +} + +async function readFilesFromProject(params: { + projectRoot: string + filePaths: string[] +}) { + const { projectRoot, filePaths } = params + const files: Record = {} + for (const filePath of filePaths) { + const fileContent = readMockFile(projectRoot, filePath) + files[filePath] = fileContent + } + return files +} - // Send successful response back to backend - return { - output: toolResult.content, - } - } catch (error) { - // Send error response back to backend - const resultString = - error instanceof Error ? error.message : String(error) - const output = [ - { - type: 'json', - value: { errorMessage: resultString }, +async function executeToolCall( + toolCall: ClientToolCall, + projectRoot: string, +): Promise { + switch (toolCall.toolName) { + case 'write_file': + case 'str_replace': + return ToolHelpers.changeFile({ + parameters: toolCall.input, + cwd: projectRoot, + fs: defaultFs, + }) + case 'run_terminal_command': { + const resolvedCwd = path.resolve( + projectRoot, + (toolCall.input as { cwd?: string }).cwd ?? '.', + ) + return ToolHelpers.runTerminalCommand({ + ...(toolCall.input as any), + cwd: resolvedCwd, + }) + } + case 'code_search': + return ToolHelpers.codeSearch({ + ...(toolCall.input as any), + projectPath: projectRoot, + }) + case 'list_directory': + return ToolHelpers.listDirectory({ + directoryPath: (toolCall.input as { path: string }).path, + projectPath: projectRoot, + fs: defaultFs, + }) + case 'glob': + return ToolHelpers.glob({ + ...(toolCall.input as any), + projectPath: projectRoot, + fs: defaultFs, + }) + case 'run_file_change_hooks': + return ToolHelpers.runFileChangeHooks(toolCall.input as any) + case 'browser_logs': + case 'create_plan': + return [ + { + type: 'json', + value: { + message: `Tool ${toolCall.toolName} is a no-op in eval scaffolding.`, }, - ] satisfies ToolResultOutput[] - toolResults.push({ - role: 'tool', - toolName: toolCall.toolName, - toolCallId: toolCall.toolCallId, - content: output, - }) - return { output } - } - }) satisfies typeof requestToolCallWs, - })) + }, + ] + case 'ask_user': + return [ + { + type: 'json', + value: { + errorMessage: 'ask_user is not supported in eval scaffolding', + }, + }, + ] + default: + return [ + { + type: 'json', + value: { + errorMessage: 'Unsupported tool in eval scaffolding', + }, + }, + ] + } } export async function getProjectFileContext( projectPath: string, ): Promise { + projectRootForMocks = projectPath const fileTree = await getProjectFileTree({ projectRoot: projectPath, fs: fs.promises, @@ -160,28 +198,43 @@ export async function runAgentStepScaffolding( sessionId: string, agentType: AgentTemplateType, ) { - const mockWs = new EventEmitter() as WebSocket - mockWs.send = mock() - mockWs.close = mock() - let fullResponse = '' + const projectRoot = getActiveProjectRoot(fileContext) const { agentTemplates: localAgentTemplates } = assembleLocalAgentTemplates({ fileContext, logger: console, }) const agentRuntimeScopedImpl: AgentRuntimeScopedDeps = { - handleStepsLogChunk: (params) => - handleStepsLogChunkWs({ ...params, ws: mockWs }), - requestToolCall: (params) => requestToolCallWs({ ...params, ws: mockWs }), - requestMcpToolData: (params) => - requestMcpToolDataWs({ ...params, ws: mockWs }), - requestFiles: (params) => requestFilesWs({ ...params, ws: mockWs }), - requestOptionalFile: (params) => - requestOptionalFileWs({ ...params, ws: mockWs }), - sendSubagentChunk: (params) => - sendSubagentChunkWs({ ...params, ws: mockWs }), - sendAction: (params) => sendActionWs({ ...params, ws: mockWs }), + handleStepsLogChunk: () => {}, + requestToolCall: async ({ toolName, input }) => { + const parsedToolCall = clientToolCallSchema.parse({ toolName, input }) + const toolCall: ClientToolCall = { + ...(parsedToolCall as ClientToolCall), + toolCallId: generateCompactId(), + } + toolCalls.push(toolCall) + const output = await executeToolCall(toolCall, projectRoot) + toolResults.push({ + role: 'tool', + toolName: toolCall.toolName, + toolCallId: toolCall.toolCallId, + content: output, + }) + return { output } + }, + requestMcpToolData: async () => [], + requestFiles: ({ filePaths }) => + readFilesFromProject({ projectRoot, filePaths }), + requestOptionalFile: async ({ filePath }) => { + const files = await readFilesFromProject({ + projectRoot, + filePaths: [filePath], + }) + return files[filePath] ?? null + }, + sendSubagentChunk: () => {}, + sendAction: () => {}, apiKey: process.env[API_KEY_ENV_VAR] ?? '', } const result = await runAgentStep({ @@ -226,8 +279,17 @@ export async function runAgentStepScaffolding( export async function runToolCalls(toolCalls: ClientToolCall[]) { const toolResults: ToolMessage[] = [] for (const toolCall of toolCalls) { - const toolResult = await handleToolCall(toolCall) - toolResults.push(toolResult) + const toolCallId = toolCall.toolCallId ?? generateCompactId() + const output = await executeToolCall( + { ...toolCall, toolCallId } as ClientToolCall, + getActiveProjectRoot(), + ) + toolResults.push({ + role: 'tool', + toolName: toolCall.toolName, + toolCallId, + content: output, + }) } return toolResults } @@ -235,14 +297,12 @@ export async function runToolCalls(toolCalls: ClientToolCall[]) { export async function loopMainPrompt({ sessionState, prompt, - projectPath, maxIterations, stopCondition, agentType, }: { sessionState: SessionState prompt: string - projectPath: string maxIterations: number stopCondition?: (sessionState: AgentState) => boolean agentType: AgentTemplateType diff --git a/evals/subagents/eval-planner.ts b/evals/subagents/eval-planner.ts index 18f02138ee..620735dc8f 100644 --- a/evals/subagents/eval-planner.ts +++ b/evals/subagents/eval-planner.ts @@ -1,13 +1,18 @@ import * as fs from 'fs' import * as path from 'path' -import { createTwoFilesPatch } from 'diff' -import { CodebuffClient, AgentDefinition } from '@codebuff/sdk' -import { getUserCredentials } from '@codebuff/npm-app/credentials' import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants' -import { loadLocalAgents } from '@codebuff/npm-app/agents/load-agents' +import { + CodebuffClient, + getUserCredentials, + loadLocalAgents, +} from '@codebuff/sdk' +import { createTwoFilesPatch } from 'diff' + import { withTestRepo } from './test-repo-utils' +import type { AgentDefinition } from '@codebuff/sdk' + export const evalPlannerAgent = async (params: { client: CodebuffClient agentId: string diff --git a/evals/test-setup.ts b/evals/test-setup.ts index b4f198c16f..3c1e1bcbf0 100644 --- a/evals/test-setup.ts +++ b/evals/test-setup.ts @@ -3,11 +3,6 @@ import fs from 'fs' import path from 'path' import { getInitialSessionState } from '@codebuff/common/types/session-state' -import { - setProjectRoot, - setWorkingDirectory, -} from '@codebuff/npm-app/project-files' -import { recreateShell } from '@codebuff/npm-app/terminal/run-command' import { createFileReadingMock, @@ -29,7 +24,6 @@ export const SWE_BENCH_PYTHON_PATH = path.join( // Mock required environment variables for tests export function setupTestEnvironmentVariables() { // Set up mock environment variables needed for tests - process.env.GOOGLE_CLOUD_PROJECT_ID = 'mock-project-id' // Add other required environment variables as needed } @@ -154,10 +148,8 @@ export async function setupTestEnvironment(projectName: string) { } const repoPath = path.join(TEST_REPOS_DIR, projectName) - setProjectRoot(repoPath) await createFileReadingMock(repoPath) - recreateShell(repoPath) - setWorkingDirectory(repoPath) + process.chdir(repoPath) // Return project info for use in tests return { diff --git a/evals/tsconfig.json b/evals/tsconfig.json index e3923bb3d5..cc5b3df041 100644 --- a/evals/tsconfig.json +++ b/evals/tsconfig.json @@ -10,5 +10,5 @@ } }, "include": ["**/*.ts"], - "exclude": ["node_modules", "test-repos", "../npm-app"] + "exclude": ["node_modules", "test-repos"] } diff --git a/knowledge.md b/knowledge.md index 0a284c0c47..d286a25a78 100644 --- a/knowledge.md +++ b/knowledge.md @@ -12,33 +12,27 @@ Codebuff is a tool for editing codebases via natural language instruction to Buf - **TypeScript**: Primary programming language - **Bun**: Package manager and runtime -- **WebSockets**: Real-time communication between client and server - **LLMs**: Multiple providers (Anthropic, OpenAI, Gemini, etc.) for various coding tasks ## Main Components 1. **LLM Integration**: Processes natural language instructions and generates code changes -2. **WebSocket Server**: Handles real-time communication between client and backend -3. **File Management**: Reads, parses, and modifies project files -4. **Action Handling**: Processes various client and server actions -5. **Knowledge Management**: Handles creation, updating, and organization of knowledge files -6. **Terminal Command Execution**: Allows running shell commands in user's terminal +2. **File Management**: Reads, parses, and modifies project files +3. **Action Handling**: Processes various client and server actions +4. **Knowledge Management**: Handles creation, updating, and organization of knowledge files +5. **Terminal Command Execution**: Allows running shell commands in user's terminal -## WebSocket Communication Flow +## API Flow -1. Client connects to WebSocket server -2. Client sends user input and file context to server -3. Server processes input using LLMs -4. Server streams response chunks back to client -5. Client receives and displays response in real-time -6. Server sends file changes to client for application +1. The SDK/CLI sends user input and file context to the Codebuff web API. +2. The agent runtime processes the request and streams response chunks back through the SDK callbacks. +3. Tools run locally via the SDK's helpers (file edits, terminal commands, search) to satisfy model tool calls. ## Tool Handling System -- Tools are defined in `backend/src/tools/definitions/list.ts` and implemented in `npm-app/src/tool-handlers.ts` +- Tools are defined in `common/src/tools` and executed via the SDK tool helpers and agent runtime - Available tools: read_files, write_file, str_replace, run_terminal_command, code_search, browser_logs, spawn_agents, web_search, read_docs, run_file_change_hooks, and others -- Backend uses tool calls to request additional information or perform actions -- Client-side handles tool calls and sends results back to server +- Tool calls request additional information or perform actions based on the current project state ## Agent System @@ -172,12 +166,10 @@ tmux attach -t git-rebase ## Error Handling and Debugging - Error messages are logged to console and debug log files -- WebSocket errors are caught and logged in server and client code ## Security Considerations - Project uses environment variables for sensitive information (API keys) -- WebSocket connections should be secured in production (WSS) - User input is validated and sanitized before processing - File operations are restricted to project directory @@ -342,12 +334,11 @@ Important constants are centralized in `common/src/constants.ts`: ## Referral System -**IMPORTANT**: Referral codes must be applied through the npm-app CLI, not through the web interface. +**IMPORTANT**: Referral codes must be applied through the CLI, not through the web interface. - Web onboarding flow shows instructions for entering codes in CLI - Users must type their referral code in the Codebuff terminal after login - Auto-redemption during web login was removed to prevent abuse -- The `handleReferralCode` function in `npm-app/src/client.ts` handles CLI redemption - The `redeemReferralCode` function in `web/src/app/api/referrals/helpers.ts` processes the actual credit granting ### OAuth Referral Code Preservation diff --git a/npm-app/.gitignore b/npm-app/.gitignore deleted file mode 100644 index 19683f6198..0000000000 --- a/npm-app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin -bin-external -dist \ No newline at end of file diff --git a/npm-app/bunfig.toml b/npm-app/bunfig.toml deleted file mode 100644 index 30c5ec33e1..0000000000 --- a/npm-app/bunfig.toml +++ /dev/null @@ -1,15 +0,0 @@ -[install] -# Don't save dependencies since this is a workspace package -no_save = true - -[loader] -# Treat .scm files as text for tree-sitter queries -".scm" = "text" - -[define] -# Define a global for the project root to help with path resolution -"process.env.PROJECT_ROOT" = "`${process.cwd()}/../`" - -[debug] -# Show more information about module resolution -show_resolution_errors = true diff --git a/npm-app/knowledge.md b/npm-app/knowledge.md deleted file mode 100644 index 3b3af82f54..0000000000 --- a/npm-app/knowledge.md +++ /dev/null @@ -1,3 +0,0 @@ -# npm-app Knowledge - -- npm distribution scripts (e.g. `release` artifacts in `npm-app/release*`) still rely on Node-based uninstall helpers for compatibility with end users. The development workflows now require Bun 1.3.0+, so keep the legacy Node snippets only in the published package files. diff --git a/npm-app/package.json b/npm-app/package.json deleted file mode 100644 index 0be041a6e2..0000000000 --- a/npm-app/package.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "@codebuff/npm-app", - "version": "1.0.0", - "private": true, - "description": "cli for codebuff", - "license": "MIT", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "exports": { - "./*": { - "bun": "./src/*.ts", - "import": "./src/*.ts", - "types": "./src/*.ts", - "default": "./src/*.ts" - } - }, - "bin": { - "codebuff": "dist/index.js" - }, - "scripts": { - "typecheck": "tsc --noEmit -p .", - "build": "bun run scripts/build-binary.js codebuff $(node -p \"require('./release/package.json').version\")", - "release": "bun run scripts/release.js", - "release-legacy": "bun run scripts/release-legacy.js", - "start-bin": "bun run build && ./bin/codebuff", - "start": "bun run src/index.ts --cwd ..", - "format": "prettier --write \"**/*.{ts,tsx,json,md}\"", - "postinstall": "bun scripts/patch-web-tree-sitter.ts" - }, - "files": [ - "README.md" - ], - "engines": { - "bun": "^1.3.0" - }, - "dependencies": { - "@codebuff/code-map": "workspace:*", - "@codebuff/common": "workspace:*", - "@types/diff": "8.0.0", - "@types/micromatch": "^4.0.9", - "@vscode/ripgrep": "^1.17.0", - "ai": "5.0.0", - "axios": "1.7.4", - "cli-highlight": "^2.1.11", - "commander": "^13.1.0", - "diff": "8.0.2", - "git-url-parse": "^16.1.0", - "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", - "nanoid": "5.0.7", - "onetime": "5.1.2", - "open": "^10.2.0", - "picocolors": "1.1.0", - "pino": "9.4.0", - "posthog-node": "4.17.2", - "puppeteer-core": "^24.2.0", - "string-width": "^7.2.0", - "systeminformation": "5.23.4", - "ts-pattern": "5.3.1", - "wrap-ansi": "^9.0.0", - "ws": "8.18.0", - "zod": "3.25.67" - }, - "devDependencies": { - "@types/markdown-it": "^14.1.2" - } -} diff --git a/npm-app/release-legacy/README.md b/npm-app/release-legacy/README.md deleted file mode 100644 index 7a67b98d69..0000000000 --- a/npm-app/release-legacy/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# The most powerful coding agent - -Codebuff is a CLI tool that writes code for you. - -1. Run `codebuff` from your project directory -2. Tell it what to do -3. It will read and write to files and run commands to produce the code you want - -Note: Codebuff will run commands in your terminal as it deems necessary to fulfill your request. - -## Installation - -To install Codebuff legacy, run: - -```bash -npm install -g codebuff@legacy -``` - -(Use `sudo` if you get a permission error.) - -## Usage - -After installation, you can start Codebuff by running: - -```bash -codebuff [project-directory] -``` - -If no project directory is specified, Codebuff will use the current directory. - -Once running, simply chat with Codebuff to say what coding task you want done. - -## Features - -- Understands your whole codebase -- Creates and edits multiple files based on your request -- Can run your tests or type checker or linter; can install packages -- It's powerful: ask Codebuff to keep working until it reaches a condition and it will. - -Our users regularly use Codebuff to implement new features, write unit tests, refactor code,write scripts, or give advice. - -## Knowledge Files - -To unlock the full benefits of modern LLMs, we recommend storing knowledge alongside your code. Add a `knowledge.md` file anywhere in your project to provide helpful context, guidance, and tips for the LLM as it performs tasks for you. - -Codebuff can fluently read and write files, so it will add knowledge as it goes. You don't need to write knowledge manually! - -Some have said every change should be paired with a unit test. In 2024, every change should come with a knowledge update! - -## Tips - -1. Type '/help' or just '/' to see available commands. -2. Create a `knowledge.md` file and collect specific points of advice. The assistant will use this knowledge to improve its responses. -3. Type `undo` or `redo` to revert or reapply file changes from the conversation. -4. Press `Esc` or `Ctrl+C` while Codebuff is generating a response to stop it. - -## Troubleshooting - -If you are getting permission errors during installation, try using sudo: - -``` -sudo npm install -g codebuff -``` - -If you still have errors, it's a good idea to [reinstall Node](https://nodejs.org/en/download). - -## Feedback - -We value your input! Please email your feedback to `founders@codebuff.com`. Thank you for using Codebuff! diff --git a/npm-app/release-legacy/index.js b/npm-app/release-legacy/index.js deleted file mode 100644 index 081cc22342..0000000000 --- a/npm-app/release-legacy/index.js +++ /dev/null @@ -1,448 +0,0 @@ -#!/usr/bin/env node - -const { spawn } = require('child_process') -const fs = require('fs') -const https = require('https') -const os = require('os') -const path = require('path') -const zlib = require('zlib') - -const { Command } = require('commander') -const tar = require('tar') - -const CONFIG = { - homeDir: os.homedir(), - configDir: path.join(os.homedir(), '.config', 'manicode'), - binaryName: process.platform === 'win32' ? 'codebuff.exe' : 'codebuff', - githubRepo: 'CodebuffAI/codebuff', - userAgent: 'codebuff-cli', - requestTimeout: 20000, -} - -CONFIG.binaryPath = path.join(CONFIG.configDir, CONFIG.binaryName) - -// Platform target mapping -const PLATFORM_TARGETS = { - 'linux-x64': 'codebuff-linux-x64.tar.gz', - 'linux-arm64': 'codebuff-linux-arm64.tar.gz', - 'darwin-x64': 'codebuff-darwin-x64.tar.gz', - 'darwin-arm64': 'codebuff-darwin-arm64.tar.gz', - 'win32-x64': 'codebuff-win32-x64.tar.gz', -} - -// Terminal utilities -let isPrintMode = false -const term = { - clearLine: () => { - if (!isPrintMode && process.stderr.isTTY) { - process.stderr.write('\r\x1b[K') - } - }, - write: (text) => { - if (!isPrintMode) { - term.clearLine() - process.stderr.write(text) - } - }, - writeLine: (text) => { - if (!isPrintMode) { - term.clearLine() - process.stderr.write(text + '\n') - } - }, -} - -// Utility functions -function httpGet(url, options = {}) { - return new Promise((resolve, reject) => { - const parsedUrl = new URL(url) - const reqOptions = { - hostname: parsedUrl.hostname, - path: parsedUrl.pathname + parsedUrl.search, - headers: { - 'User-Agent': CONFIG.userAgent, - ...options.headers, - }, - } - - // Add GitHub token if available - const token = process.env.GITHUB_TOKEN - if (token) { - reqOptions.headers.Authorization = `Bearer ${token}` - } - - const req = https.get(reqOptions, (res) => { - if (res.statusCode === 302 || res.statusCode === 301) { - return httpGet(new URL(res.headers.location, url).href, options) - .then(resolve) - .catch(reject) - } - resolve(res) - }) - - req.on('error', reject) - - const timeout = options.timeout || CONFIG.requestTimeout - req.setTimeout(timeout, () => { - req.destroy() - reject(new Error('Request timeout.')) - }) - }) -} - -async function getLatestLegacyVersion() { - try { - const res = await httpGet(`https://registry.npmjs.org/codebuff/legacy`) - - if (res.statusCode !== 200) return null - - const body = await streamToString(res) - const packageData = JSON.parse(body) - - return packageData.version || null - } catch (error) { - return null - } -} - -function streamToString(stream) { - return new Promise((resolve, reject) => { - let data = '' - stream.on('data', (chunk) => (data += chunk)) - stream.on('end', () => resolve(data)) - stream.on('error', reject) - }) -} - -function getCurrentVersion() { - return new Promise((resolve, reject) => { - try { - if (!fs.existsSync(CONFIG.binaryPath)) { - resolve('error') - return - } - - const child = spawn(CONFIG.binaryPath, ['--version'], { - cwd: os.homedir(), - stdio: 'pipe', - }) - - let output = '' - let errorOutput = '' - - child.stdout.on('data', (data) => { - output += data.toString() - }) - - child.stderr.on('data', (data) => { - errorOutput += data.toString() - }) - - const timeout = setTimeout(() => { - child.kill('SIGTERM') - setTimeout(() => { - if (!child.killed) { - child.kill('SIGKILL') - } - }, 1000) - resolve('error') - }, 1000) - - child.on('exit', (code) => { - clearTimeout(timeout) - if (code === 0) { - resolve(output.trim()) - } else { - resolve('error') - } - }) - - child.on('error', () => { - clearTimeout(timeout) - resolve('error') - }) - } catch (error) { - resolve('error') - } - }) -} - -function compareVersions(v1, v2) { - if (!v1 || !v2) return 0 - - if (!v1.includes('legacy')) { - return -1 - } - - const parts1 = v1.replace('-legacy', '').split('.').map(Number) - const parts2 = v2.replace('-legacy', '').split('.').map(Number) - - for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { - const p1 = parts1[i] || 0 - const p2 = parts2[i] || 0 - - if (p1 < p2) return -1 - if (p1 > p2) return 1 - } - - return 0 -} - -function formatBytes(bytes) { - if (bytes === 0) return '0 B' - const k = 1024 - const sizes = ['B', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] -} - -function createProgressBar(percentage, width = 30) { - const filled = Math.round((width * percentage) / 100) - const empty = width - filled - return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']' -} - -async function downloadBinary(version) { - const platformKey = `${process.platform}-${process.arch}` - const fileName = PLATFORM_TARGETS[platformKey] - - if (!fileName) { - throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`) - } - - // Use proxy endpoint that handles version mapping - const downloadUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL - ? `${process.env.NEXT_PUBLIC_CODEBUFF_APP_URL}/api/releases/download/${version}/${fileName}` - : `https://codebuff.com/api/releases/download/${version}/${fileName}` - - // Ensure config directory exists - fs.mkdirSync(CONFIG.configDir, { recursive: true }) - - if (fs.existsSync(CONFIG.binaryPath)) { - fs.unlinkSync(CONFIG.binaryPath) - } - - term.write('Downloading...') - - const res = await httpGet(downloadUrl) - - if (res.statusCode !== 200) { - throw new Error(`Download failed: HTTP ${res.statusCode}`) - } - - const totalSize = parseInt(res.headers['content-length'] || '0', 10) - let downloadedSize = 0 - let lastProgressTime = Date.now() - - res.on('data', (chunk) => { - downloadedSize += chunk.length - const now = Date.now() - if (now - lastProgressTime >= 100 || downloadedSize === totalSize) { - lastProgressTime = now - if (totalSize > 0) { - const pct = Math.round((downloadedSize / totalSize) * 100) - term.write( - `Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes( - totalSize, - )}`, - ) - } else { - term.write(`Downloading... ${formatBytes(downloadedSize)}`) - } - } - }) - - await new Promise((resolve, reject) => { - res - .pipe(zlib.createGunzip()) - .pipe(tar.x({ cwd: CONFIG.configDir })) - .on('finish', resolve) - .on('error', reject) - }) - - try { - // Find the extracted binary - it should be named "codebuff" or "codebuff.exe" - const files = fs.readdirSync(CONFIG.configDir) - const extractedPath = path.join(CONFIG.configDir, CONFIG.binaryName) - - if (fs.existsSync(extractedPath)) { - if (process.platform !== 'win32') { - fs.chmodSync(extractedPath, 0o755) - } - } else { - throw new Error( - `Binary not found after extraction. Expected: ${extractedPath}, Available files: ${files.join(', ')}`, - ) - } - } catch (error) { - term.clearLine() - if (!isPrintMode) { - console.error(`Extraction failed: ${error.message}`) - } - process.exit(1) - } - - term.clearLine() - if (isPrintMode) { - console.log( - JSON.stringify({ type: 'download', version, status: 'complete' }), - ) - } else { - console.log('Download complete! Starting Codebuff...') - } -} - -async function ensureBinaryExists() { - const currentVersion = await getCurrentVersion() - if (currentVersion !== null && currentVersion !== 'error') { - return - } - - const version = await getLatestLegacyVersion() - if (!version) { - if (isPrintMode) { - console.error( - JSON.stringify({ - type: 'error', - message: 'Failed to determine latest version.', - }), - ) - } else { - console.error('❌ Failed to determine latest version') - console.error('Please check your internet connection and try again') - } - process.exit(1) - } - - try { - await downloadBinary(version) - } catch (error) { - term.clearLine() - if (isPrintMode) { - console.error( - JSON.stringify({ - type: 'error', - message: `Failed to download codebuff: ${error.message}`, - }), - ) - } else { - console.error('❌ Failed to download codebuff:', error.message) - console.error('Please check your internet connection and try again') - } - process.exit(1) - } -} - -async function checkForUpdates(runningProcess, exitListener, retry) { - try { - const currentVersion = await getCurrentVersion() - - const latestVersion = await getLatestLegacyVersion() - if (!latestVersion) return - - if ( - // Download new version if current binary errors. - currentVersion === 'error' || - compareVersions(currentVersion, latestVersion) < 0 - ) { - term.clearLine() - - // Remove the specific exit listener to prevent it from interfering with the update - runningProcess.removeListener('exit', exitListener) - - // Kill the running process - runningProcess.kill('SIGTERM') - - // Wait for the process to actually exit - await new Promise((resolve) => { - runningProcess.on('exit', resolve) - // Fallback timeout in case the process doesn't exit gracefully - setTimeout(() => { - if (!runningProcess.killed) { - runningProcess.kill('SIGKILL') - } - resolve() - }, 5000) - }) - - if (!isPrintMode) { - console.log(`Update available: ${currentVersion} → ${latestVersion}`) - } - - await downloadBinary(latestVersion) - - await retry(isPrintMode) - } - } catch (error) { - // Silently ignore update check errors - } -} - -async function main(firstRun = false, printMode = false) { - console.log('\x1b[1m\x1b[91m' + '='.repeat(54) + '\x1b[0m') - console.log('\x1b[1m\x1b[93m ❄️ CODEBUFF LEGACY UI ❄️\x1b[0m') - console.log( - '\x1b[1m\x1b[91mRUN `npm i -g codebuff@latest` TO SWITCH TO THE NEW UI\x1b[0m', - ) - console.log('\x1b[1m\x1b[91m' + '='.repeat(54) + '\x1b[0m') - console.log('') - - isPrintMode = printMode - await ensureBinaryExists() - - let error = null - try { - // Start codebuff - const child = spawn(CONFIG.binaryPath, process.argv.slice(2), { - stdio: 'inherit', - }) - - // Store reference to the exit listener so we can remove it during updates - const exitListener = (code) => { - process.exit(code || 0) - } - - child.on('exit', exitListener) - - if (firstRun) { - // Check for updates in background - setTimeout(() => { - if (!error) { - checkForUpdates(child, exitListener, () => main(false, isPrintMode)) - } - }, 100) - } - } catch (err) { - error = err - if (firstRun) { - if (!isPrintMode) { - console.error('❌ Codebuff failed to start:', error.message) - console.log('Redownloading Codebuff...') - } - // Binary could be corrupted (killed before download completed), so delete and retry. - fs.unlinkSync(CONFIG.binaryPath) - await main(false, isPrintMode) - } - } -} - -// Setup commander -const program = new Command() -program - .name('codebuff') - .description('AI coding agent') - .helpOption(false) - .option('-p, --print', 'print mode - suppress wrapper output') - .allowUnknownOption() - .parse() - -const options = program.opts() -isPrintMode = options.print - -// Run the main function -main(true, isPrintMode).catch((error) => { - if (!isPrintMode) { - console.error('❌ Unexpected error:', error.message) - } - process.exit(1) -}) diff --git a/npm-app/release-legacy/package.json b/npm-app/release-legacy/package.json deleted file mode 100644 index 37138e1833..0000000000 --- a/npm-app/release-legacy/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "codebuff", - "version": "0.0.6-legacy.0", - "description": "AI coding agent", - "license": "MIT", - "bin": { - "codebuff": "index.js", - "cb": "index.js" - }, - "scripts": { - "postinstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'codebuff.exe' : 'codebuff'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\"", - "preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'codebuff.exe' : 'codebuff'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\"" - }, - "files": [ - "index.js", - "README.md" - ], - "os": [ - "darwin", - "linux", - "win32" - ], - "cpu": [ - "x64", - "arm64" - ], - "engines": { - "node": ">=16" - }, - "dependencies": { - "commander": "^12.0.0", - "tar": "^6.2.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/CodebuffAI/codebuff.git" - }, - "homepage": "https://codebuff.com", - "publishConfig": { - "access": "public" - } -} diff --git a/npm-app/release-staging/README.md b/npm-app/release-staging/README.md deleted file mode 100644 index ecb2a8503e..0000000000 --- a/npm-app/release-staging/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# 🚀 Codecane - The most powerful coding agent (STAGING) - -**⚠️ This is a staging/beta release for testing purposes.** - -Codecane is a CLI tool that writes code for you. - -1. Run `codecane` from your project directory -2. Tell it what to do -3. It will read and write to files and run commands to produce the code you want - -Note: Codecane will run commands in your terminal as it deems necessary to fulfill your request. - -## Installation - -To install Codecane (staging), run: - -```bash -npm install -g codecane@beta -``` - -(Use `sudo` if you get a permission error.) - -## Usage - -After installation, you can start Codecane by running: - -```bash -codecane [project-directory] -``` - -If no project directory is specified, Codecane will use the current directory. - -Once running, simply chat with Codecane to say what coding task you want done. - -## Features - -- Understands your whole codebase -- Creates and edits multiple files based on your request -- Can run your tests or type checker or linter; can install packages -- It's powerful: ask Codecane to keep working until it reaches a condition and it will. - -Our users regularly use Codecane to implement new features, write unit tests, refactor code,write scripts, or give advice. - -## Knowledge Files - -To unlock the full benefits of modern LLMs, we recommend storing knowledge alongside your code. Add a `knowledge.md` file anywhere in your project to provide helpful context, guidance, and tips for the LLM as it performs tasks for you. - -Codecane can fluently read and write files, so it will add knowledge as it goes. You don't need to write knowledge manually! - -Some have said every change should be paired with a unit test. In 2024, every change should come with a knowledge update! - -## Tips - -1. Type '/help' or just '/' to see available commands. -2. Create a `knowledge.md` file and collect specific points of advice. The assistant will use this knowledge to improve its responses. -3. Type `undo` or `redo` to revert or reapply file changes from the conversation. -4. Press `Esc` or `Ctrl+C` while Codecane is generating a response to stop it. - -## Troubleshooting - -If you are getting permission errors during installation, try using sudo: - -``` -sudo npm install -g codecane@beta -``` - -If you still have errors, it's a good idea to [reinstall Node](https://nodejs.org/en/download). - -## Feedback - -We value your input! Please email your feedback to `founders@codebuff.com`. Thank you for using Codecane! - - diff --git a/npm-app/release-staging/index.js b/npm-app/release-staging/index.js deleted file mode 100644 index ace74003d4..0000000000 --- a/npm-app/release-staging/index.js +++ /dev/null @@ -1,440 +0,0 @@ -#!/usr/bin/env node - -const { spawn } = require('child_process') -const fs = require('fs') -const https = require('https') -const os = require('os') -const path = require('path') -const zlib = require('zlib') - -const tar = require('tar') - -// Hardcoded package name for codecane -const packageName = 'codecane' - -function createConfig(packageName) { - const homeDir = os.homedir() - const configDir = path.join(homeDir, '.config', 'manicode') - const binaryName = - process.platform === 'win32' ? `${packageName}.exe` : packageName - - return { - homeDir, - configDir, - binaryName, - binaryPath: path.join(configDir, binaryName), - - userAgent: `${packageName}-cli`, - requestTimeout: 20000, - } -} - -const CONFIG = createConfig(packageName) - -// Platform target mapping -const PLATFORM_TARGETS = { - 'linux-x64': `${packageName}-linux-x64.tar.gz`, - 'linux-arm64': `${packageName}-linux-arm64.tar.gz`, - 'darwin-x64': `${packageName}-darwin-x64.tar.gz`, - 'darwin-arm64': `${packageName}-darwin-arm64.tar.gz`, - 'win32-x64': `${packageName}-win32-x64.tar.gz`, -} - -// Terminal utilities -const term = { - clearLine: () => { - if (process.stderr.isTTY) { - process.stderr.write('\r\x1b[K') - } - }, - write: (text) => { - term.clearLine() - process.stderr.write(text) - }, - writeLine: (text) => { - term.clearLine() - process.stderr.write(text + '\n') - }, -} - -// Utility functions -function httpGet(url, options = {}) { - return new Promise((resolve, reject) => { - const parsedUrl = new URL(url) - const reqOptions = { - hostname: parsedUrl.hostname, - path: parsedUrl.pathname + parsedUrl.search, - headers: { - 'User-Agent': CONFIG.userAgent, - ...options.headers, - }, - } - - const req = https.get(reqOptions, (res) => { - if (res.statusCode === 302 || res.statusCode === 301) { - return httpGet(new URL(res.headers.location, url).href, options) - .then(resolve) - .catch(reject) - } - resolve(res) - }) - - req.on('error', reject) - - const timeout = options.timeout || CONFIG.requestTimeout - req.setTimeout(timeout, () => { - req.destroy() - reject(new Error('Request timeout.')) - }) - }) -} - -async function getLatestVersion() { - try { - // Use the direct /latest endpoint for stable releases - const res = await httpGet( - `https://registry.npmjs.org/${packageName}/latest`, - ) - - if (res.statusCode !== 200) return null - - const body = await streamToString(res) - const packageData = JSON.parse(body) - - return packageData.version || null - } catch (error) { - return null - } -} - -function streamToString(stream) { - return new Promise((resolve, reject) => { - let data = '' - stream.on('data', (chunk) => (data += chunk)) - stream.on('end', () => resolve(data)) - stream.on('error', reject) - }) -} - -function getCurrentVersion() { - if (!fs.existsSync(CONFIG.binaryPath)) return null - - try { - return new Promise((resolve, reject) => { - const child = spawn(CONFIG.binaryPath, ['--version'], { - cwd: os.homedir(), - stdio: 'pipe', - }) - - let output = '' - let errorOutput = '' - - child.stdout.on('data', (data) => { - output += data.toString() - }) - - child.stderr.on('data', (data) => { - errorOutput += data.toString() - }) - - const timeout = setTimeout(() => { - child.kill('SIGTERM') - setTimeout(() => { - if (!child.killed) { - child.kill('SIGKILL') - } - }, 1000) - resolve('error') - }, 1000) - - child.on('exit', (code) => { - clearTimeout(timeout) - if (code === 0) { - resolve(output.trim()) - } else { - resolve('error') - } - }) - - child.on('error', () => { - clearTimeout(timeout) - resolve('error') - }) - }) - } catch (error) { - return 'error' - } -} - -function compareVersions(v1, v2) { - if (!v1 || !v2) return 0 - - const parseVersion = (version) => { - const parts = version.split('-') - const mainParts = parts[0].split('.').map(Number) - const prereleaseParts = parts[1] ? parts[1].split('.') : [] - return { main: mainParts, prerelease: prereleaseParts } - } - - const p1 = parseVersion(v1) - const p2 = parseVersion(v2) - - // Compare main version parts - for (let i = 0; i < Math.max(p1.main.length, p2.main.length); i++) { - const n1 = p1.main[i] || 0 - const n2 = p2.main[i] || 0 - - if (n1 < n2) return -1 - if (n1 > n2) return 1 - } - - // If main versions are equal, compare prerelease parts - if (p1.prerelease.length === 0 && p2.prerelease.length === 0) { - return 0 // No prerelease, versions are equal - } else if (p1.prerelease.length === 0) { - return 1 // v1 is a release, v2 is prerelease, so v1 > v2 - } else if (p2.prerelease.length === 0) { - return -1 // v2 is a release, v1 is prerelease, so v1 < v2 - } else { - // Both have prerelease parts, compare them - for ( - let i = 0; - i < Math.max(p1.prerelease.length, p2.prerelease.length); - i++ - ) { - const pr1 = p1.prerelease[i] || '' - const pr2 = p2.prerelease[i] || '' - - // Handle numeric vs. string parts - const isNum1 = !isNaN(parseInt(pr1)) - const isNum2 = !isNaN(parseInt(pr2)) - - if (isNum1 && isNum2) { - const num1 = parseInt(pr1) - const num2 = parseInt(pr2) - if (num1 < num2) return -1 - if (num1 > num2) return 1 - } else if (isNum1 && !isNum2) { - return 1 // Numeric prerelease is generally higher than alpha/beta - } else if (!isNum1 && isNum2) { - return -1 - } else { - // Lexicographical comparison for string parts - if (pr1 < pr2) return -1 - if (pr1 > pr2) return 1 - } - } - return 0 // Prerelease parts are equal - } -} - -function formatBytes(bytes) { - if (bytes === 0) return '0 B' - const k = 1024 - const sizes = ['B', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] -} - -function createProgressBar(percentage, width = 30) { - const filled = Math.round((width * percentage) / 100) - const empty = width - filled - return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']' -} - -async function downloadBinary(version) { - const platformKey = `${process.platform}-${process.arch}` - const fileName = PLATFORM_TARGETS[platformKey] - - if (!fileName) { - throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`) - } - - // Use proxy endpoint that handles version mapping from npm to GitHub releases - const downloadUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL - ? `${process.env.NEXT_PUBLIC_CODEBUFF_APP_URL}/api/releases/download/${version}/${fileName}` - : `https://codebuff.com/api/releases/download/${version}/${fileName}` - - // Ensure config directory exists - fs.mkdirSync(CONFIG.configDir, { recursive: true }) - - if (fs.existsSync(CONFIG.binaryPath)) { - fs.unlinkSync(CONFIG.binaryPath) - } - - term.write('Downloading...') - - const res = await httpGet(downloadUrl) - - if (res.statusCode !== 200) { - throw new Error(`Download failed: HTTP ${res.statusCode}`) - } - - const totalSize = parseInt(res.headers['content-length'] || '0', 10) - let downloadedSize = 0 - let lastProgressTime = Date.now() - - res.on('data', (chunk) => { - downloadedSize += chunk.length - const now = Date.now() - if (now - lastProgressTime >= 100 || downloadedSize === totalSize) { - lastProgressTime = now - if (totalSize > 0) { - const pct = Math.round((downloadedSize / totalSize) * 100) - term.write( - `Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes( - totalSize, - )}`, - ) - } else { - term.write(`Downloading... ${formatBytes(downloadedSize)}`) - } - } - }) - - await new Promise((resolve, reject) => { - res - .pipe(zlib.createGunzip()) - .pipe(tar.x({ cwd: CONFIG.configDir })) - .on('finish', resolve) - .on('error', reject) - }) - - try { - // Find the extracted binary - it should be named "codebuff" or "codebuff.exe" - const files = fs.readdirSync(CONFIG.configDir) - const extractedPath = path.join(CONFIG.configDir, CONFIG.binaryName) - - if (fs.existsSync(extractedPath)) { - if (process.platform !== 'win32') { - fs.chmodSync(extractedPath, 0o755) - } - } else { - throw new Error( - `Binary not found after extraction. Expected: ${extractedPath}, Available files: ${files.join(', ')}`, - ) - } - } catch (error) { - term.clearLine() - console.error(`Extraction failed: ${error.message}`) - process.exit(1) - } - - term.clearLine() - console.log('Download complete! Starting Codecane...') -} - -async function ensureBinaryExists() { - const currentVersion = await getCurrentVersion() - if (currentVersion !== null && currentVersion !== 'error') { - return - } - - const version = await getLatestVersion() - if (!version) { - console.error('❌ Failed to determine latest version') - console.error('Please check your internet connection and try again') - process.exit(1) - } - - try { - await downloadBinary(version) - } catch (error) { - term.clearLine() - console.error('❌ Failed to download codebuff:', error.message) - console.error('Please check your internet connection and try again') - process.exit(1) - } -} - -async function checkForUpdates(runningProcess, exitListener) { - try { - const currentVersion = await getCurrentVersion() - if (!currentVersion) return - - const latestVersion = await getLatestVersion() - if (!latestVersion) return - - if ( - // Download new version if current binary errors. - currentVersion === 'error' || - compareVersions(currentVersion, latestVersion) < 0 - ) { - term.clearLine() - - // Remove the specific exit listener to prevent it from interfering with the update - runningProcess.removeListener('exit', exitListener) - - // Kill the running process - runningProcess.kill('SIGTERM') - - // Wait for the process to actually exit - await new Promise((resolve) => { - runningProcess.on('exit', resolve) - // Fallback timeout in case the process doesn't exit gracefully - setTimeout(() => { - if (!runningProcess.killed) { - runningProcess.kill('SIGKILL') - } - resolve() - }, 5000) - }) - - console.log(`Update available: ${currentVersion} → ${latestVersion}`) - - await downloadBinary(latestVersion) - - // Restart with new binary - this replaces the current process - const newChild = spawn(CONFIG.binaryPath, process.argv.slice(2), { - stdio: 'inherit', - detached: false, - }) - - // Set up exit handler for the new process - newChild.on('exit', (code) => { - process.exit(code || 0) - }) - - // Don't return - keep this function running to maintain the wrapper - return new Promise(() => {}) // Never resolves, keeps wrapper alive - } - } catch (error) { - // Silently ignore update check errors - } -} - -async function main() { - // Bold, bright warning for staging environment - console.log('\x1b[1m\x1b[91m' + '='.repeat(60) + '\x1b[0m') - console.log('\x1b[1m\x1b[93m❄️ CODECANE STAGING ENVIRONMENT ❄️\x1b[0m') - console.log( - '\x1b[1m\x1b[91mFOR TESTING PURPOSES ONLY - NOT FOR PRODUCTION USE\x1b[0m', - ) - console.log('\x1b[1m\x1b[91m' + '='.repeat(60) + '\x1b[0m') - console.log('') - - await ensureBinaryExists() - - // Start the binary with codecane argument - const child = spawn(CONFIG.binaryPath, process.argv.slice(2), { - stdio: 'inherit', - }) - - // Store reference to the exit listener so we can remove it during updates - const exitListener = (code) => { - process.exit(code || 0) - } - - child.on('exit', exitListener) - - // Check for updates in background - setTimeout(() => { - checkForUpdates(child, exitListener) - }, 100) -} - -// Run the main function -main().catch((error) => { - console.error('❌ Unexpected error:', error.message) - process.exit(1) -}) diff --git a/npm-app/release-staging/package.json b/npm-app/release-staging/package.json deleted file mode 100644 index e3b4d1127e..0000000000 --- a/npm-app/release-staging/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "codecane", - "version": "1.0.420", - "description": "AI coding agent (staging)", - "license": "MIT", - "bin": { - "codecane": "index.js" - }, - "scripts": { - "preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'codecane.exe' : 'codecane'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\"" - }, - "files": [ - "index.js", - "README.md" - ], - "os": [ - "darwin", - "linux", - "win32" - ], - "cpu": [ - "x64", - "arm64" - ], - "engines": { - "node": ">=16" - }, - "dependencies": { - "tar": "^6.2.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/CodebuffAI/codebuff-community.git" - }, - "homepage": "https://codebuff.com", - "publishConfig": { - "access": "public" - } -} diff --git a/npm-app/release/README.md b/npm-app/release/README.md deleted file mode 100644 index e2afcdb63a..0000000000 --- a/npm-app/release/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# The most powerful coding agent - -Codebuff is a CLI tool that writes code for you. - -1. Run `codebuff` from your project directory -2. Tell it what to do -3. It will read and write to files and run commands to produce the code you want - -Note: Codebuff will run commands in your terminal as it deems necessary to fulfill your request. - -## Installation - -To install Codebuff, run: - -```bash -npm install -g codebuff -``` - -(Use `sudo` if you get a permission error.) - -## Usage - -After installation, you can start Codebuff by running: - -```bash -codebuff [project-directory] -``` - -If no project directory is specified, Codebuff will use the current directory. - -Once running, simply chat with Codebuff to say what coding task you want done. - -## Features - -- Understands your whole codebase -- Creates and edits multiple files based on your request -- Can run your tests or type checker or linter; can install packages -- It's powerful: ask Codebuff to keep working until it reaches a condition and it will. - -Our users regularly use Codebuff to implement new features, write unit tests, refactor code,write scripts, or give advice. - -## Knowledge Files - -To unlock the full benefits of modern LLMs, we recommend storing knowledge alongside your code. Add a `knowledge.md` file anywhere in your project to provide helpful context, guidance, and tips for the LLM as it performs tasks for you. - -Codebuff can fluently read and write files, so it will add knowledge as it goes. You don't need to write knowledge manually! - -Some have said every change should be paired with a unit test. In 2024, every change should come with a knowledge update! - -## Tips - -1. Type '/help' or just '/' to see available commands. -2. Create a `knowledge.md` file and collect specific points of advice. The assistant will use this knowledge to improve its responses. -3. Type `undo` or `redo` to revert or reapply file changes from the conversation. -4. Press `Esc` or `Ctrl+C` while Codebuff is generating a response to stop it. - -## Troubleshooting - -If you are getting permission errors during installation, try using sudo: - -``` -sudo npm install -g codebuff -``` - -If you still have errors, it's a good idea to [reinstall Node](https://nodejs.org/en/download). - -## Feedback - -We value your input! Please email your feedback to `founders@codebuff.com`. Thank you for using Codebuff! diff --git a/npm-app/release/index.js b/npm-app/release/index.js deleted file mode 100644 index c6a73b8b78..0000000000 --- a/npm-app/release/index.js +++ /dev/null @@ -1,440 +0,0 @@ -#!/usr/bin/env node - -const { spawn } = require('child_process') -const fs = require('fs') -const https = require('https') -const os = require('os') -const path = require('path') -const zlib = require('zlib') - -const { Command } = require('commander') -const tar = require('tar') - -const CONFIG = { - homeDir: os.homedir(), - configDir: path.join(os.homedir(), '.config', 'manicode'), - binaryName: process.platform === 'win32' ? 'codebuff.exe' : 'codebuff', - githubRepo: 'CodebuffAI/codebuff', - userAgent: 'codebuff-cli', - requestTimeout: 20000, -} - -CONFIG.binaryPath = path.join(CONFIG.configDir, CONFIG.binaryName) - -// Platform target mapping -const PLATFORM_TARGETS = { - 'linux-x64': 'codebuff-linux-x64.tar.gz', - 'linux-arm64': 'codebuff-linux-arm64.tar.gz', - 'darwin-x64': 'codebuff-darwin-x64.tar.gz', - 'darwin-arm64': 'codebuff-darwin-arm64.tar.gz', - 'win32-x64': 'codebuff-win32-x64.tar.gz', -} - -// Terminal utilities -let isPrintMode = false -const term = { - clearLine: () => { - if (!isPrintMode && process.stderr.isTTY) { - process.stderr.write('\r\x1b[K') - } - }, - write: (text) => { - if (!isPrintMode) { - term.clearLine() - process.stderr.write(text) - } - }, - writeLine: (text) => { - if (!isPrintMode) { - term.clearLine() - process.stderr.write(text + '\n') - } - }, -} - -// Utility functions -function httpGet(url, options = {}) { - return new Promise((resolve, reject) => { - const parsedUrl = new URL(url) - const reqOptions = { - hostname: parsedUrl.hostname, - path: parsedUrl.pathname + parsedUrl.search, - headers: { - 'User-Agent': CONFIG.userAgent, - ...options.headers, - }, - } - - // Add GitHub token if available - const token = process.env.GITHUB_TOKEN - if (token) { - reqOptions.headers.Authorization = `Bearer ${token}` - } - - const req = https.get(reqOptions, (res) => { - if (res.statusCode === 302 || res.statusCode === 301) { - return httpGet(new URL(res.headers.location, url).href, options) - .then(resolve) - .catch(reject) - } - resolve(res) - }) - - req.on('error', reject) - - const timeout = options.timeout || CONFIG.requestTimeout - req.setTimeout(timeout, () => { - req.destroy() - reject(new Error('Request timeout.')) - }) - }) -} - -async function getLatestVersion() { - try { - const res = await httpGet(`https://registry.npmjs.org/codebuff/latest`) - - if (res.statusCode !== 200) return null - - const body = await streamToString(res) - const packageData = JSON.parse(body) - - return packageData.version || null - } catch (error) { - return null - } -} - -function streamToString(stream) { - return new Promise((resolve, reject) => { - let data = '' - stream.on('data', (chunk) => (data += chunk)) - stream.on('end', () => resolve(data)) - stream.on('error', reject) - }) -} - -function getCurrentVersion() { - return new Promise((resolve, reject) => { - try { - if (!fs.existsSync(CONFIG.binaryPath)) { - resolve('error') - return - } - - const child = spawn(CONFIG.binaryPath, ['--version'], { - cwd: os.homedir(), - stdio: 'pipe', - }) - - let output = '' - let errorOutput = '' - - child.stdout.on('data', (data) => { - output += data.toString() - }) - - child.stderr.on('data', (data) => { - errorOutput += data.toString() - }) - - const timeout = setTimeout(() => { - child.kill('SIGTERM') - setTimeout(() => { - if (!child.killed) { - child.kill('SIGKILL') - } - }, 1000) - resolve('error') - }, 1000) - - child.on('exit', (code) => { - clearTimeout(timeout) - if (code === 0) { - resolve(output.trim()) - } else { - resolve('error') - } - }) - - child.on('error', () => { - clearTimeout(timeout) - resolve('error') - }) - } catch (error) { - resolve('error') - } - }) -} - -function compareVersions(v1, v2) { - if (!v1 || !v2) return 0 - - if (!v1.match(/^\d+(\.\d+)*$/)) { - return -1 - } - - const parts1 = v1.split('.').map(Number) - const parts2 = v2.split('.').map(Number) - - for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { - const p1 = parts1[i] || 0 - const p2 = parts2[i] || 0 - - if (p1 < p2) return -1 - if (p1 > p2) return 1 - } - - return 0 -} - -function formatBytes(bytes) { - if (bytes === 0) return '0 B' - const k = 1024 - const sizes = ['B', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] -} - -function createProgressBar(percentage, width = 30) { - const filled = Math.round((width * percentage) / 100) - const empty = width - filled - return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']' -} - -async function downloadBinary(version) { - const platformKey = `${process.platform}-${process.arch}` - const fileName = PLATFORM_TARGETS[platformKey] - - if (!fileName) { - throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`) - } - - // Use proxy endpoint that handles version mapping - const downloadUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL - ? `${process.env.NEXT_PUBLIC_CODEBUFF_APP_URL}/api/releases/download/${version}/${fileName}` - : `https://codebuff.com/api/releases/download/${version}/${fileName}` - - // Ensure config directory exists - fs.mkdirSync(CONFIG.configDir, { recursive: true }) - - if (fs.existsSync(CONFIG.binaryPath)) { - fs.unlinkSync(CONFIG.binaryPath) - } - - term.write('Downloading...') - - const res = await httpGet(downloadUrl) - - if (res.statusCode !== 200) { - throw new Error(`Download failed: HTTP ${res.statusCode}`) - } - - const totalSize = parseInt(res.headers['content-length'] || '0', 10) - let downloadedSize = 0 - let lastProgressTime = Date.now() - - res.on('data', (chunk) => { - downloadedSize += chunk.length - const now = Date.now() - if (now - lastProgressTime >= 100 || downloadedSize === totalSize) { - lastProgressTime = now - if (totalSize > 0) { - const pct = Math.round((downloadedSize / totalSize) * 100) - term.write( - `Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes( - totalSize, - )}`, - ) - } else { - term.write(`Downloading... ${formatBytes(downloadedSize)}`) - } - } - }) - - await new Promise((resolve, reject) => { - res - .pipe(zlib.createGunzip()) - .pipe(tar.x({ cwd: CONFIG.configDir })) - .on('finish', resolve) - .on('error', reject) - }) - - try { - // Find the extracted binary - it should be named "codebuff" or "codebuff.exe" - const files = fs.readdirSync(CONFIG.configDir) - const extractedPath = path.join(CONFIG.configDir, CONFIG.binaryName) - - if (fs.existsSync(extractedPath)) { - if (process.platform !== 'win32') { - fs.chmodSync(extractedPath, 0o755) - } - } else { - throw new Error( - `Binary not found after extraction. Expected: ${extractedPath}, Available files: ${files.join(', ')}`, - ) - } - } catch (error) { - term.clearLine() - if (!isPrintMode) { - console.error(`Extraction failed: ${error.message}`) - } - process.exit(1) - } - - term.clearLine() - if (isPrintMode) { - console.log( - JSON.stringify({ type: 'download', version, status: 'complete' }), - ) - } else { - console.log('Download complete! Starting Codebuff...') - } -} - -async function ensureBinaryExists() { - const currentVersion = await getCurrentVersion() - if (currentVersion !== null && currentVersion !== 'error') { - return - } - - const version = await getLatestVersion() - if (!version) { - if (isPrintMode) { - console.error( - JSON.stringify({ - type: 'error', - message: 'Failed to determine latest version.', - }), - ) - } else { - console.error('❌ Failed to determine latest version') - console.error('Please check your internet connection and try again') - } - process.exit(1) - } - - try { - await downloadBinary(version) - } catch (error) { - term.clearLine() - if (isPrintMode) { - console.error( - JSON.stringify({ - type: 'error', - message: `Failed to download codebuff: ${error.message}`, - }), - ) - } else { - console.error('❌ Failed to download codebuff:', error.message) - console.error('Please check your internet connection and try again') - } - process.exit(1) - } -} - -async function checkForUpdates(runningProcess, exitListener, retry) { - try { - const currentVersion = await getCurrentVersion() - - const latestVersion = await getLatestVersion() - if (!latestVersion) return - - if ( - // Download new version if current binary errors. - currentVersion === 'error' || - compareVersions(currentVersion, latestVersion) < 0 - ) { - term.clearLine() - - // Remove the specific exit listener to prevent it from interfering with the update - runningProcess.removeListener('exit', exitListener) - - // Kill the running process - runningProcess.kill('SIGTERM') - - // Wait for the process to actually exit - await new Promise((resolve) => { - runningProcess.on('exit', resolve) - // Fallback timeout in case the process doesn't exit gracefully - setTimeout(() => { - if (!runningProcess.killed) { - runningProcess.kill('SIGKILL') - } - resolve() - }, 5000) - }) - - if (!isPrintMode) { - console.log(`Update available: ${currentVersion} → ${latestVersion}`) - } - - await downloadBinary(latestVersion) - - await retry(isPrintMode) - } - } catch (error) { - // Silently ignore update check errors - } -} - -async function main(firstRun = false, printMode = false) { - isPrintMode = printMode - await ensureBinaryExists() - - let error = null - try { - // Start codebuff - const child = spawn(CONFIG.binaryPath, process.argv.slice(2), { - stdio: 'inherit', - }) - - // Store reference to the exit listener so we can remove it during updates - const exitListener = (code) => { - process.exit(code || 0) - } - - child.on('exit', exitListener) - - if (firstRun) { - // Check for updates in background - setTimeout(() => { - if (!error) { - checkForUpdates(child, exitListener, () => main(false, isPrintMode)) - } - }, 100) - } - } catch (err) { - error = err - if (firstRun) { - if (!isPrintMode) { - console.error('❌ Codebuff failed to start:', error.message) - console.log('Redownloading Codebuff...') - } - // Binary could be corrupted (killed before download completed), so delete and retry. - fs.unlinkSync(CONFIG.binaryPath) - await main(false, isPrintMode) - } - } -} - -// Setup commander -const program = new Command() -program - .name('codebuff') - .description('AI coding agent') - .helpOption(false) - .option('-p, --print', 'print mode - suppress wrapper output') - .allowUnknownOption() - .parse() - -const options = program.opts() -isPrintMode = options.print - -// Run the main function -main(true, isPrintMode).catch((error) => { - if (!isPrintMode) { - console.error('❌ Unexpected error:', error.message) - } - process.exit(1) -}) diff --git a/npm-app/release/package.json b/npm-app/release/package.json deleted file mode 100644 index 39d66bd53d..0000000000 --- a/npm-app/release/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "codebuff", - "version": "1.0.513", - "description": "AI coding agent", - "license": "MIT", - "bin": { - "codebuff": "index.js", - "cb": "index.js" - }, - "scripts": { - "preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'codebuff.exe' : 'codebuff'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\"" - }, - "files": [ - "index.js", - "README.md" - ], - "os": [ - "darwin", - "linux", - "win32" - ], - "cpu": [ - "x64", - "arm64" - ], - "engines": { - "node": ">=16" - }, - "dependencies": { - "commander": "^12.0.0", - "tar": "^6.2.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/CodebuffAI/codebuff.git" - }, - "homepage": "https://codebuff.com", - "publishConfig": { - "access": "public" - } -} diff --git a/npm-app/scripts/build-binary.js b/npm-app/scripts/build-binary.js deleted file mode 100644 index 9dd5e4ec32..0000000000 --- a/npm-app/scripts/build-binary.js +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env node - -const { execSync } = require('child_process') -const fs = require('fs') -const path = require('path') - -const { patchWebTreeSitter } = require('./patch-web-tree-sitter.js') - -// Configuration -const VERBOSE = process.env.VERBOSE === 'true' - -// Get package name and npm app version from command line arguments -const packageName = process.argv[2] || 'codebuff' -const npmAppVersion = process.argv[3] - -if (!packageName) { - console.error('Error: Package name is required as first argument') - console.error('Usage: build-binary.js ') - process.exit(1) -} - -if (!npmAppVersion) { - console.error('Error: NPM app version is required as second argument') - console.error('Usage: build-binary.js ') - process.exit(1) -} - -// Logging helper -function log(message) { - if (VERBOSE) { - console.log(message) - } -} - -function logAlways(message) { - console.log(message) -} - -// Get current platform info -const currentPlatform = process.platform -const currentArch = process.arch - -// Map current platform/arch to target info -const getTargetInfo = () => { - // Check for environment variable overrides (for cross-compilation) - if ( - process.env.OVERRIDE_TARGET && - process.env.OVERRIDE_PLATFORM && - process.env.OVERRIDE_ARCH - ) { - return { - bunTarget: process.env.OVERRIDE_TARGET, - platform: process.env.OVERRIDE_PLATFORM, - arch: process.env.OVERRIDE_ARCH, - } - } - - const platformKey = `${currentPlatform}-${currentArch}` - - const targetMap = { - 'linux-x64': { bunTarget: 'bun-linux-x64', platform: 'linux', arch: 'x64' }, - 'linux-arm64': { - bunTarget: 'bun-linux-arm64', - platform: 'linux', - arch: 'arm64', - }, - 'darwin-x64': { - bunTarget: 'bun-darwin-x64', - platform: 'darwin', - arch: 'x64', - }, - 'darwin-arm64': { - bunTarget: 'bun-darwin-arm64', - platform: 'darwin', - arch: 'arm64', - }, - 'win32-x64': { - bunTarget: 'bun-windows-x64', - platform: 'win32', - arch: 'x64', - }, - } - - const targetInfo = targetMap[platformKey] - if (!targetInfo) { - console.error(`Unsupported platform: ${platformKey}`) - process.exit(1) - } - - return targetInfo -} - -async function main() { - log('🔧 Patching web-tree-sitter...') - patchWebTreeSitter(VERBOSE) - - const targetInfo = getTargetInfo() - const outputName = - currentPlatform === 'win32' ? `${packageName}.exe` : packageName - - await buildTarget(targetInfo.bunTarget, outputName, targetInfo) -} - -async function buildTarget(bunTarget, outputName, targetInfo) { - // Create bin directory - const binDir = path.join(__dirname, '..', 'bin') - if (!fs.existsSync(binDir)) { - fs.mkdirSync(binDir, { recursive: true }) - } - - const outputFile = path.join(binDir, outputName) - - log( - `🔨 Building ${outputName} (${targetInfo.platform}-${targetInfo.arch})...`, - ) - - const flags = { - IS_BINARY: 'true', - NEXT_PUBLIC_NPM_APP_VERSION: npmAppVersion, - } - - const defineFlags = Object.entries(flags) - .map(([key, value]) => { - const stringValue = - typeof value === 'string' ? `'${value}'` : String(value) - return `--define process.env.${key}=${JSON.stringify(stringValue)}` - }) - .join(' ') - - const entrypoints = [ - 'src/index.ts', - 'src/workers/project-context.ts', - 'src/workers/checkpoint-worker.ts', - ] - - const command = [ - 'bun build --compile', - ...entrypoints, - `--target=${bunTarget}`, - '--asset-naming=[name].[ext]', - defineFlags, - '--env "NEXT_PUBLIC_*"', // Copies all current env vars in process.env to the compiled binary that match the pattern. - `--outfile=${outputFile}`, - // '--minify', // harder to debug - ] - .filter(Boolean) - .join(' ') - - try { - const stdio = VERBOSE ? 'inherit' : 'pipe' - execSync(command, { stdio, shell: true }) - - // Make executable on Unix systems - if (!outputName.endsWith('.exe')) { - fs.chmodSync(outputFile, 0o755) - } - - logAlways( - `✅ Built ${outputName} for ${targetInfo.platform}-${targetInfo.arch}`, - ) - } catch (error) { - logAlways(`❌ Failed to build ${outputName}: ${error.message}`) - process.exit(1) - } -} - -main().catch((error) => { - console.error('Build failed:', error) - process.exit(1) -}) diff --git a/npm-app/scripts/patch-web-tree-sitter.ts b/npm-app/scripts/patch-web-tree-sitter.ts deleted file mode 100644 index b833f66a18..0000000000 --- a/npm-app/scripts/patch-web-tree-sitter.ts +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env node -import fs from 'fs' -import path from 'path' -import { fileURLToPath } from 'url' - -/* - * This script patches web-tree-sitter to use inlined WASM data - * instead of file system access for better binary compatibility. - */ - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -function patchSingleFile(webTreeSitterPath: string, verbose: boolean): boolean { - if (!fs.existsSync(webTreeSitterPath)) { - if (verbose) { - console.warn( - `⚠️ web-tree-sitter not found at ${webTreeSitterPath}, skipping`, - ) - } - return false - } - - try { - let content = fs.readFileSync(webTreeSitterPath, 'utf8') - const originalContent = content - - if (verbose) { - console.log(`Checking file at: ${webTreeSitterPath}`) - console.log('File size:', content.length) - } - - // Read and encode the WASM file as base64 - const wasmPath = path.join( - __dirname, - '../../node_modules/web-tree-sitter/tree-sitter.wasm', - ) - if (!fs.existsSync(wasmPath)) { - throw new Error(`❌ Web-tree-sitter WASM file not found at ${wasmPath}`) - } - const wasmBuffer = fs.readFileSync(wasmPath) - const wasmBase64 = wasmBuffer.toString('base64') - - // Check if already patched with the new version - if (content.includes('CODEBUFF_PATCHED_FINDWASM_V3')) { - if (verbose) { - console.log('ℹ️ Already patched with new version') - } - return false - } - - // Remove old patches completely - restore original file first - // Reinstall the package to get a clean version - if (verbose) { - console.log('🔄 Removing old patches, reinstalling web-tree-sitter...') - } - const { execSync } = require('child_process') - execSync('bun uninstall web-tree-sitter', { - cwd: path.join(__dirname, '../..'), - stdio: 'pipe', - }) - execSync('bun install web-tree-sitter@0.25.6', { - cwd: path.join(__dirname, '../../packages/code-map'), - stdio: 'pipe', - }) - - // Re-read the clean file - content = fs.readFileSync(webTreeSitterPath, 'utf8') - - // Add global WASM data at the top of the file - const globalWasmData = ` -// CODEBUFF_PATCHED_GLOBAL_WASM -var CODEBUFF_INLINED_WASM_DATA = "${wasmBase64}"; -var CODEBUFF_WASM_BINARY = null; -` - - // Insert the global data after the first line - const lines = content.split('\n') - lines.splice(1, 0, globalWasmData) - content = lines.join('\n') - - // Track replacement success - const replacements = [] - - // Patch pattern for readFileSync - const readPattern = - 'var ret = fs.readFileSync(filename, binary2 ? void 0 : "utf8");' - const readReplacement = `/*CODEBUFF_PATCHED*/var ret; if(typeof Bun!=="undefined"&&binary2&&filename.includes("tree-sitter.wasm")&&typeof CODEBUFF_INLINED_WASM_DATA!=="undefined"){ret=new Uint8Array(Buffer.from(CODEBUFF_INLINED_WASM_DATA,"base64"));}else{ret=fs.readFileSync(filename, binary2 ? void 0 : "utf8");}` - - const newContent1 = content.replace(readPattern, readReplacement) - replacements.push({ - name: 'readFileSync patch', - success: newContent1 !== content, - }) - content = newContent1 - - // Patch the getBinarySync function to use our inlined data - const getBinarySyncPattern = - /function getBinarySync\(file\) \{\s*if \(file == wasmBinaryFile && wasmBinary\) \{\s*return new Uint8Array\(wasmBinary\);\s*\}/ - const getBinarySyncReplacement = `function getBinarySync(file) { - /*CODEBUFF_PATCHED_GETBINARY*/ - if (typeof Bun !== "undefined" && typeof CODEBUFF_INLINED_WASM_DATA !== "undefined") { - if (!CODEBUFF_WASM_BINARY) { - CODEBUFF_WASM_BINARY = new Uint8Array(Buffer.from(CODEBUFF_INLINED_WASM_DATA, "base64")); - } - return CODEBUFF_WASM_BINARY; - } - if (file == wasmBinaryFile && wasmBinary) { - return new Uint8Array(wasmBinary); - }` - - const newContent2 = content.replace( - getBinarySyncPattern, - getBinarySyncReplacement, - ) - replacements.push({ - name: 'getBinarySync patch', - success: newContent2 !== content, - }) - content = newContent2 - - // Patch pattern for findWasmBinary function - simplified approach - const findWasmPattern = - /function findWasmBinary\(\) \{\s*if \(Module\["locateFile"\]\) \{\s*return locateFile\("tree-sitter\.wasm"\);\s*\}\s*return new URL\("tree-sitter\.wasm", import\.meta\.url\)\.href;\s*\}/ - const findWasmReplacement = `function findWasmBinary() { - /*CODEBUFF_PATCHED_FINDWASM_V3*/ - if (typeof Bun !== "undefined" && typeof CODEBUFF_INLINED_WASM_DATA !== "undefined") { - // Set wasmBinary directly so getBinarySync can use it - if (!CODEBUFF_WASM_BINARY) { - CODEBUFF_WASM_BINARY = Buffer.from(CODEBUFF_INLINED_WASM_DATA, "base64"); - } - wasmBinary = CODEBUFF_WASM_BINARY; - wasmBinaryFile = "tree-sitter.wasm"; - return "tree-sitter.wasm"; - } - if (Module["locateFile"]) { - return locateFile("tree-sitter.wasm"); - } - return new URL("tree-sitter.wasm", import.meta.url).href; - }` - - const newContent3 = content.replace(findWasmPattern, findWasmReplacement) - replacements.push({ - name: 'findWasmBinary patch', - success: newContent3 !== content, - }) - content = newContent3 - - // Check if all replacements were successful - const failedReplacements = replacements.filter((r) => !r.success) - - if (content !== originalContent) { - fs.writeFileSync(webTreeSitterPath, content, 'utf8') - if (verbose) { - console.log('✅ Patched successfully with inlined WASM data') - if (failedReplacements.length > 0) { - console.warn( - `⚠️ Some patches failed: ${failedReplacements.map((r) => r.name).join(', ')}`, - ) - } - } - return true - } else { - if (verbose) { - console.log( - '⚠️ No changes made - all patterns may have failed to match', - ) - console.log( - `Failed replacements: ${failedReplacements.map((r) => r.name).join(', ')}`, - ) - } - return false - } - } catch (error) { - console.error(`❌ Failed to patch ${webTreeSitterPath}:`, error.message) - return false - } -} - -export function patchWebTreeSitter(verbose = false) { - // Only patch root node_modules (hoisted) - const webTreeSitterPath = path.join( - __dirname, - '../../node_modules/web-tree-sitter/tree-sitter.js', - ) - - let patchedCount = 0 - if (patchSingleFile(webTreeSitterPath, verbose)) { - patchedCount++ - } - - if (verbose) { - console.log(`✅ Patched ${patchedCount} web-tree-sitter file(s)`) - } -} - -// Check if this script is being run directly -if (import.meta.url === `file://${process.argv[1]}`) { - patchWebTreeSitter(true) -} diff --git a/npm-app/scripts/release-legacy.js b/npm-app/scripts/release-legacy.js deleted file mode 100755 index aaf0895639..0000000000 --- a/npm-app/scripts/release-legacy.js +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env node - -const { execSync } = require('child_process') - -// Parse command line arguments -const args = process.argv.slice(2) -const versionType = args[0] || 'prepatch' // prepatch, minor, major, or specific version like 1.2.3 - -function log(message) { - console.log(`${message}`) -} - -function error(message) { - console.error(`❌ ${message}`) - process.exit(1) -} - -function formatTimestamp() { - const now = new Date() - const options = { - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - timeZoneName: 'short', - } - return now.toLocaleDateString('en-US', options) -} - -function checkGitHubToken() { - const token = process.env.CODEBUFF_GITHUB_TOKEN - if (!token) { - error( - 'CODEBUFF_GITHUB_TOKEN environment variable is required but not set.\n' + - 'Please set it with your GitHub personal access token or use the infisical setup.', - ) - } - - // Set GITHUB_TOKEN for compatibility with existing curl commands - process.env.GITHUB_TOKEN = token - return token -} - -async function triggerWorkflow(versionType) { - if (!process.env.GITHUB_TOKEN) { - error('GITHUB_TOKEN environment variable is required but not set') - } - - try { - // Use workflow filename instead of ID - const triggerCmd = `curl -s -w "HTTP Status: %{http_code}" -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${process.env.GITHUB_TOKEN}" \ - -H "Content-Type: application/json" \ - https://api.github.com/repos/CodebuffAI/codebuff/actions/workflows/npm-app-release-legacy.yml/dispatches \ - -d '{"ref":"main","inputs":{"version_type":"${versionType}"}}'` - - const response = execSync(triggerCmd, { encoding: 'utf8' }) - - // Check if response contains error message - if (response.includes('workflow_dispatch')) { - log(`⚠️ Workflow dispatch failed: ${response}`) - log('The workflow may need to be updated on GitHub. Continuing anyway...') - log( - 'Please manually trigger the workflow at: https://github.com/CodebuffAI/codebuff/actions/workflows/npm-app-release-legacy.yml', - ) - } else { - // log( - // `Workflow trigger response: ${response || '(empty response - likely success)'}` - // ) - log('🎉 Release workflow triggered!') - } - } catch (err) { - log(`⚠️ Failed to trigger workflow automatically: ${err.message}`) - log( - 'You may need to trigger it manually at: https://github.com/CodebuffAI/codebuff/actions/workflows/npm-app-release-legacy.yml', - ) - } -} - -async function main() { - log('🚀 Initiating release...') - log(`Date: ${formatTimestamp()}`) - - // Check for local GitHub token - checkGitHubToken() - log('✅ Using local CODEBUFF_GITHUB_TOKEN') - - log(`Version bump type: ${versionType}`) - - // Trigger the workflow - await triggerWorkflow(versionType) - - log('') - log('Monitor progress at: https://github.com/CodebuffAI/codebuff/actions') -} - -main().catch((err) => { - error(`Release failed: ${err.message}`) -}) diff --git a/npm-app/scripts/release.js b/npm-app/scripts/release.js deleted file mode 100755 index 9d35cac9a8..0000000000 --- a/npm-app/scripts/release.js +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env node - -const { execSync } = require('child_process') - -// Parse command line arguments -const args = process.argv.slice(2) -const versionType = args[0] || 'patch' // patch, minor, major, or specific version like 1.2.3 - -function log(message) { - console.log(`${message}`) -} - -function error(message) { - console.error(`❌ ${message}`) - process.exit(1) -} - -function formatTimestamp() { - const now = new Date() - const options = { - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - timeZoneName: 'short', - } - return now.toLocaleDateString('en-US', options) -} - -function checkGitHubToken() { - const token = process.env.CODEBUFF_GITHUB_TOKEN - if (!token) { - error( - 'CODEBUFF_GITHUB_TOKEN environment variable is required but not set.\n' + - 'Please set it with your GitHub personal access token or use the infisical setup.' - ) - } - - // Set GITHUB_TOKEN for compatibility with existing curl commands - process.env.GITHUB_TOKEN = token - return token -} - -async function triggerWorkflow(versionType) { - if (!process.env.GITHUB_TOKEN) { - error('GITHUB_TOKEN environment variable is required but not set') - } - - try { - // Use workflow filename instead of ID - const triggerCmd = `curl -s -w "HTTP Status: %{http_code}" -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${process.env.GITHUB_TOKEN}" \ - -H "Content-Type: application/json" \ - https://api.github.com/repos/CodebuffAI/codebuff/actions/workflows/npm-app-release-prod.yml/dispatches \ - -d '{"ref":"main","inputs":{"version_type":"${versionType}"}}'` - - const response = execSync(triggerCmd, { encoding: 'utf8' }) - - // Check if response contains error message - if (response.includes('workflow_dispatch')) { - log(`⚠️ Workflow dispatch failed: ${response}`) - log('The workflow may need to be updated on GitHub. Continuing anyway...') - log( - 'Please manually trigger the workflow at: https://github.com/CodebuffAI/codebuff/actions/workflows/npm-app-release-prod.yml', - ) - } else { - // log( - // `Workflow trigger response: ${response || '(empty response - likely success)'}` - // ) - log('🎉 Release workflow triggered!') - } - } catch (err) { - log(`⚠️ Failed to trigger workflow automatically: ${err.message}`) - log( - 'You may need to trigger it manually at: https://github.com/CodebuffAI/codebuff/actions/workflows/npm-app-release-prod.yml', - ) - } -} - -async function main() { - log('🚀 Initiating release...') - log(`Date: ${formatTimestamp()}`) - - // Check for local GitHub token - checkGitHubToken() - log('✅ Using local CODEBUFF_GITHUB_TOKEN') - - log(`Version bump type: ${versionType}`) - - // Trigger the workflow - await triggerWorkflow(versionType) - - log('') - log('Monitor progress at: https://github.com/CodebuffAI/codebuff/actions') -} - -main().catch((err) => { - error(`Release failed: ${err.message}`) -}) diff --git a/npm-app/scripts/twitch-plays-codebuff.sh b/npm-app/scripts/twitch-plays-codebuff.sh deleted file mode 100755 index e7de8b1e9e..0000000000 --- a/npm-app/scripts/twitch-plays-codebuff.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -function send_input() { - local input="$1" - tmux send-keys -t codebuff "$input" - sleep 1 - tmux send-keys -t codebuff Enter -} - -# Create a new tmux session named 'codebuff' and start codebuff in it -tmux new-session -d -s codebuff 'codebuff' - -# Track last message to avoid duplicates -last_message="" - -# Run every 15 seconds -while true; do - # Get timestamp from 3 minutes ago - timestamp=$(($(date +%s) * 1000 - 180000)) - - # Fetch last 10 messages from API - response=$(curl -s "https://recent-messages.robotty.de/api/v2/recent-messages/codebuff_ai?limit=10&after=$timestamp") - - # Process messages in reverse order and stop after first successful send - if [ ! -z "$response" ]; then - messages=$(echo "$response" | jq -r '.messages | reverse | .[]') - while IFS= read -r msg; do - message=$(echo "$msg" | grep -o 'PRIVMSG #codebuff_ai :>.*' | sed 's/PRIVMSG #codebuff_ai :>//') - if [ ! -z "$message" ] && [ "$message" != "$last_message" ]; then - send_input "$message" - last_message="$message" - fi - break - done <<< "$messages" - fi - - sleep 15 -done diff --git a/npm-app/src/__tests__/display.test.ts b/npm-app/src/__tests__/display.test.ts deleted file mode 100644 index 7d47223b0a..0000000000 --- a/npm-app/src/__tests__/display.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { describe, expect, it } from 'bun:test' -import { gray } from 'picocolors' - -import { onlyWhitespace, squashNewlines } from '../display/squash-newlines' - -const PREFIX = '.\r\n' - -// Helper function to simulate getLastTwoLines behavior -function getLastTwoLines(str: string): string { - return PREFIX + str.split('\r\n').slice(-2).join('\r\n') -} - -describe('squashNewlines', () => { - describe('when called with getLastTwoLines(previous) + chunk', () => { - it('should handle simple strings', () => { - const previous = 'line1\r\nline2\r\nline3' - const chunk = '\r\nline4\r\nline5' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + chunk) - }) - - it('should handle when chunk has empty lines', () => { - const previous = 'content\r\nmore content' - const chunk = '\r\n\r\n\r\nfinal line' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + '\r\n\r\nfinal line') - }) - - it('should handle when chunk has whitespace lines', () => { - const previous = 'first\r\nsecond\r\nthird' - const chunk = '\r\n \r\n\t\r\nfourth' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + '\r\n \r\n\tfourth') - }) - - it('should handle when previous is empty', () => { - const previous = '' - const chunk = 'some\r\ncontent' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + chunk) - }) - - it('should handle when chunk is empty', () => { - const previous = 'some\r\ncontent\r\nhere' - const chunk = '' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + chunk) - }) - - it('should handle when both strings are empty', () => { - const previous = '' - const chunk = '' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + chunk) - }) - - it('should handle complex mixed content', () => { - const previous = 'alpha\r\nbeta\r\ngamma\r\ndelta' - const chunk = '\r\n\r\n \r\n\t\r\n\r\nepsilon\r\nzeta' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + '\r\n\r\n \tepsilon\r\nzeta') - }) - - it('should handle when previous has only newlines', () => { - const previous = '\r\n\r\n\r\n' - const chunk = 'content' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + chunk) - }) - - it('should handle when chunk has only newlines', () => { - const previous = 'content\r\nmore' - const chunk = '\r\n\r\n\r\n' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + '\r\n\r\n') - }) - - it('should handle single line inputs', () => { - const previous = 'single line' - const chunk = 'another line' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + chunk) - }) - - it('should handle previous ends with whitespace lines', () => { - const previous = 'content\r\n \r\n\t' - const chunk = '\r\nmore content' - const lastTwoLines = getLastTwoLines(previous) - const combined = lastTwoLines + chunk - const squashed = squashNewlines(combined) - - expect(squashed).toEqual(lastTwoLines + 'more content') - }) - }) - - describe('squashNewlines behavior verification', () => { - it('should squash consecutive empty lines correctly', () => { - const input = PREFIX + 'line1\r\n\r\n\r\n\r\nline5' - const result = squashNewlines(input) - - // Should reduce multiple consecutive empty lines to at most 2 - expect(result).toBe(PREFIX + 'line1\r\n\r\nline5') - }) - - it('should preserve single empty lines', () => { - const input = PREFIX + 'line1\r\n\r\nline3' - const result = squashNewlines(input) - - expect(result).toBe(input) // Should remain unchanged - }) - }) - - describe('error handling', () => { - it('should throw error when input does not start with PREFIX', () => { - const invalidInput = 'invalid input without prefix' - - expect(() => squashNewlines(invalidInput)).toThrow( - `Expected string to start with ${JSON.stringify(PREFIX)}`, - ) - }) - }) -}) - -describe('onlyWhitespace', () => { - it('should return true for empty string', () => { - expect(onlyWhitespace('')).toBe(true) - }) - - it('should return true for single space', () => { - expect(onlyWhitespace(' ')).toBe(true) - }) - - it('should return true for multiple spaces', () => { - expect(onlyWhitespace(' ')).toBe(true) - }) - - it('should return true for tab', () => { - expect(onlyWhitespace('\t')).toBe(true) - }) - - it('should return true for newline', () => { - expect(onlyWhitespace('\n')).toBe(true) - }) - - it('should return true for carriage return', () => { - expect(onlyWhitespace('\r')).toBe(true) - }) - - it('should return true for ANSI escape sequences', () => { - expect(onlyWhitespace('\u001b[31m')).toBe(true) // Red color - expect(onlyWhitespace('\u001b[0m')).toBe(true) // Reset - }) - - it('should return true for OSC sequences', () => { - const oscSequence = - '\u001b]697;OSCUnlock=683fe5e7c2d2476bb61d4e0588c15eec\u0007\u001b]697;Dir=/Users/jahooma/codebuff\u0007' - expect(onlyWhitespace(oscSequence)).toBe(true) - }) - - it('should return false for control characters', () => { - expect(onlyWhitespace('\u0000\u0001\u0002')).toBe(true) // Null, SOH, STX - expect(onlyWhitespace('\u007F')).toBe(true) // DEL - }) - - it('should return true for zero-width characters', () => { - expect(onlyWhitespace('\u200B')).toBe(true) - expect(onlyWhitespace('\u200C')).toBe(true) - expect(onlyWhitespace('\u200D')).toBe(true) - }) - - it('should return true for colored empty strings', () => { - expect(onlyWhitespace(gray(' '))).toBe(true) - }) - - it('should return false for visible text', () => { - expect(onlyWhitespace('hello')).toBe(false) - expect(onlyWhitespace('a')).toBe(false) - expect(onlyWhitespace('123')).toBe(false) - }) - - describe('real world examples', () => { - it('should return false for end of terminal command', () => { - expect( - onlyWhitespace( - '\u001b]697;OSCUnlock=683fe5e7c2d2476bb61d4e0588c15eec\u0007\u001b]697;Dir=/Users/jahooma/codebuff\u0007\u001b]697;Shell=bash\u0007\u001b]697;ShellPath=/bin/bash\u0007\u001b]697;PID=71631\u0007\u001b]697;ExitCode=0\u0007\u001b]697;TTY=/dev/ttys036\u0007\u001b]697;Log=\u0007\u001b]697;User=jahooma\u0007\u001b]697;OSCLock=683fe5e7c2d2476bb61d4e0588c15eec\u0007\u001b]697;PreExec\u0007\u001b]697;StartPrompt\u0007', - ), - ).toBe(true) - - expect( - onlyWhitespace( - '\u001b]0;charles@charles-framework-13:~/github/codebuff\u001b\\\u001b]7;file://charles-framework-13/home/charles/github/codebuff\u001b\\\u001b[?2004h', - ), - ).toBe(true) - }) - }) -}) diff --git a/npm-app/src/__tests__/glob-handler.test.ts b/npm-app/src/__tests__/glob-handler.test.ts deleted file mode 100644 index 8a1af3096d..0000000000 --- a/npm-app/src/__tests__/glob-handler.test.ts +++ /dev/null @@ -1,344 +0,0 @@ -import fs from 'fs' -import path from 'path' - -import { - clearMockedModules, - mockModule, -} from '@codebuff/common/testing/mock-modules' -import { - afterAll, - afterEach, - beforeAll, - beforeEach, - describe, - expect, - mock, - test, -} from 'bun:test' - -import { toolHandlers } from '../tool-handlers' - -const handleGlob = toolHandlers.glob - -describe('handleGlob', () => { - const testDataDir = path.resolve(__dirname, 'glob-test-data') - const mockGetProjectRoot = mock(() => { - return path.resolve(__dirname, '../../') - }) - - beforeAll(async () => { - await mockModule('@codebuff/npm-app/project-files', () => ({ - getProjectRoot: mockGetProjectRoot, - })) - }) - - beforeEach(async () => { - const projectRoot = path.resolve(__dirname, '../../') - mockGetProjectRoot.mockReturnValue(projectRoot) - - // Create test data directory and nested structure - await fs.promises.mkdir(testDataDir, { recursive: true }) - await fs.promises.mkdir(path.join(testDataDir, 'src'), { recursive: true }) - await fs.promises.mkdir(path.join(testDataDir, 'src', 'components'), { - recursive: true, - }) - await fs.promises.mkdir(path.join(testDataDir, 'lib'), { recursive: true }) - await fs.promises.mkdir(path.join(testDataDir, 'docs'), { recursive: true }) - - // Create test files - await fs.promises.writeFile( - path.join(testDataDir, 'package.json'), - '{}', - ) - await fs.promises.writeFile( - path.join(testDataDir, 'README.md'), - '# Test', - ) - await fs.promises.writeFile( - path.join(testDataDir, 'src', 'index.ts'), - 'export {}', - ) - await fs.promises.writeFile( - path.join(testDataDir, 'src', 'utils.ts'), - 'export {}', - ) - await fs.promises.writeFile( - path.join(testDataDir, 'src', 'components', 'Button.tsx'), - 'export {}', - ) - await fs.promises.writeFile( - path.join(testDataDir, 'src', 'components', 'Input.tsx'), - 'export {}', - ) - await fs.promises.writeFile( - path.join(testDataDir, 'lib', 'helper.js'), - 'module.exports = {}', - ) - await fs.promises.writeFile( - path.join(testDataDir, 'docs', 'guide.md'), - '# Guide', - ) - }) - - afterEach(async () => { - try { - await fs.promises.rm(testDataDir, { recursive: true, force: true }) - } catch (error) { - // Ignore cleanup errors - } - }) - - afterAll(() => { - clearMockedModules() - }) - - test('matches all files with **/* pattern without cwd', async () => { - const parameters = { - pattern: '**/*', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - // Should match all files in the project (limited to our test structure) - expect(files.length).toBeGreaterThan(0) - expect((result[0].value as any).count).toBe(files.length) - }) - - test('matches all files with **/* pattern with cwd', async () => { - const parameters = { - pattern: '**/*', - cwd: 'src/__tests__/glob-test-data', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - // Should match all 8 files in our test directory - expect(files.length).toBe(8) - expect(files).toContain('src/__tests__/glob-test-data/package.json') - expect(files).toContain('src/__tests__/glob-test-data/README.md') - expect(files).toContain('src/__tests__/glob-test-data/src/index.ts') - expect(files).toContain('src/__tests__/glob-test-data/src/utils.ts') - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Button.tsx', - ) - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Input.tsx', - ) - expect(files).toContain('src/__tests__/glob-test-data/lib/helper.js') - expect(files).toContain('src/__tests__/glob-test-data/docs/guide.md') - }) - - test('matches specific extension with *.ts pattern', async () => { - const parameters = { - pattern: '*.ts', - cwd: 'src/__tests__/glob-test-data/src', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(2) - expect(files).toContain('src/__tests__/glob-test-data/src/index.ts') - expect(files).toContain('src/__tests__/glob-test-data/src/utils.ts') - expect(files).not.toContain( - 'src/__tests__/glob-test-data/src/components/Button.tsx', - ) - }) - - test('matches nested files with **/*.tsx pattern', async () => { - const parameters = { - pattern: '**/*.tsx', - cwd: 'src/__tests__/glob-test-data', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(2) - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Button.tsx', - ) - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Input.tsx', - ) - }) - - test('matches files in subdirectory with src/**/* pattern', async () => { - const parameters = { - pattern: 'src/**/*', - cwd: 'src/__tests__/glob-test-data', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(4) - expect(files).toContain('src/__tests__/glob-test-data/src/index.ts') - expect(files).toContain('src/__tests__/glob-test-data/src/utils.ts') - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Button.tsx', - ) - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Input.tsx', - ) - expect(files).not.toContain('src/__tests__/glob-test-data/lib/helper.js') - }) - - test('matches markdown files with **/*.md pattern', async () => { - const parameters = { - pattern: '**/*.md', - cwd: 'src/__tests__/glob-test-data', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(2) - expect(files).toContain('src/__tests__/glob-test-data/README.md') - expect(files).toContain('src/__tests__/glob-test-data/docs/guide.md') - }) - - test('matches single file with exact name', async () => { - const parameters = { - pattern: 'package.json', - cwd: 'src/__tests__/glob-test-data', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(1) - expect(files).toContain('src/__tests__/glob-test-data/package.json') - }) - - test('matches no files with non-matching pattern', async () => { - const parameters = { - pattern: '*.py', - cwd: 'src/__tests__/glob-test-data', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(0) - expect((result[0].value as any).message).toContain('Found 0 file(s)') - }) - - test('matches TypeScript files only with **/*.ts pattern', async () => { - const parameters = { - pattern: '**/*.ts', - cwd: 'src/__tests__/glob-test-data/src', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - // Should match only .ts files, not .tsx files - expect(files.length).toBe(2) - expect(files).toContain('src/__tests__/glob-test-data/src/index.ts') - expect(files).toContain('src/__tests__/glob-test-data/src/utils.ts') - expect(files).not.toContain( - 'src/__tests__/glob-test-data/src/components/Button.tsx', - ) - expect(files).not.toContain( - 'src/__tests__/glob-test-data/src/components/Input.tsx', - ) - }) - - test('matches files with brace expansion for multiple extensions', async () => { - const parameters = { - pattern: '**/*.{ts,js}', - cwd: 'src/__tests__/glob-test-data', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - // Should match all .ts and .js files recursively using brace expansion - expect(files.length).toBe(3) - expect(files).toContain('src/__tests__/glob-test-data/src/index.ts') - expect(files).toContain('src/__tests__/glob-test-data/src/utils.ts') - expect(files).toContain('src/__tests__/glob-test-data/lib/helper.js') - }) - - test('handles cwd with trailing slash', async () => { - const parameters = { - pattern: '**/*', - cwd: 'src/__tests__/glob-test-data/', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(8) - }) - - test('returns appropriate message in result', async () => { - const parameters = { - pattern: '**/*.ts', - cwd: 'src/__tests__/glob-test-data/src', - } - - const result = await handleGlob(parameters, 'test-id') - - expect((result[0].value as any).message).toContain('Found 2 file(s)') - expect((result[0].value as any).message).toContain('**/*.ts') - expect((result[0].value as any).message).toContain( - 'src/__tests__/glob-test-data/src', - ) - }) - - test('handles pattern matching in nested cwd', async () => { - const parameters = { - pattern: '*.tsx', - cwd: 'src/__tests__/glob-test-data/src/components', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(2) - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Button.tsx', - ) - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Input.tsx', - ) - }) - - test('matches all TypeScript files recursively', async () => { - const parameters = { - pattern: '**/*.ts', - cwd: 'src/__tests__/glob-test-data', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(2) - expect(files).toContain('src/__tests__/glob-test-data/src/index.ts') - expect(files).toContain('src/__tests__/glob-test-data/src/utils.ts') - }) - - test('matches with brace expansion pattern', async () => { - const parameters = { - pattern: '**/*.{ts,tsx}', - cwd: 'src/__tests__/glob-test-data', - } - - const result = await handleGlob(parameters, 'test-id') - const files = (result[0].value as any).files - - expect(files.length).toBe(4) - expect(files).toContain('src/__tests__/glob-test-data/src/index.ts') - expect(files).toContain('src/__tests__/glob-test-data/src/utils.ts') - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Button.tsx', - ) - expect(files).toContain( - 'src/__tests__/glob-test-data/src/components/Input.tsx', - ) - }) -}) diff --git a/npm-app/src/__tests__/image-upload.test.ts b/npm-app/src/__tests__/image-upload.test.ts deleted file mode 100644 index c51ff2b8c5..0000000000 --- a/npm-app/src/__tests__/image-upload.test.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { writeFileSync, mkdirSync, rmSync } from 'fs' -import path from 'path' - -import { describe, test, expect, beforeEach, afterEach } from 'bun:test' - -import { - processImageFile, - isImageFile, - extractImagePaths, -} from '../utils/image-handler' - -const TEST_DIR = path.join(__dirname, 'temp-test-images') -const TEST_IMAGE_PATH = path.join(TEST_DIR, 'test-image.png') -const TEST_LARGE_IMAGE_PATH = path.join(TEST_DIR, 'large-image.jpg') - -// Create a minimal PNG file (43 bytes) -const MINIMAL_PNG = Buffer.from([ - 0x89, - 0x50, - 0x4e, - 0x47, - 0x0d, - 0x0a, - 0x1a, - 0x0a, // PNG signature - 0x00, - 0x00, - 0x00, - 0x0d, // IHDR chunk length - 0x49, - 0x48, - 0x44, - 0x52, // IHDR - 0x00, - 0x00, - 0x00, - 0x01, // width: 1 - 0x00, - 0x00, - 0x00, - 0x01, // height: 1 - 0x08, - 0x02, - 0x00, - 0x00, - 0x00, // bit depth, color type, compression, filter, interlace - 0x90, - 0x77, - 0x53, - 0xde, // CRC - 0x00, - 0x00, - 0x00, - 0x00, // IEND chunk length - 0x49, - 0x45, - 0x4e, - 0x44, // IEND - 0xae, - 0x42, - 0x60, - 0x82, // CRC -]) - -beforeEach(() => { - mkdirSync(TEST_DIR, { recursive: true }) - writeFileSync(TEST_IMAGE_PATH, MINIMAL_PNG) - - // Create a large fake image (10MB) - const largeBuffer = Buffer.alloc(10 * 1024 * 1024, 0xff) - // Add minimal JPEG header - largeBuffer.writeUInt16BE(0xffd8, 0) // JPEG SOI marker - largeBuffer.writeUInt16BE(0xffd9, largeBuffer.length - 2) // JPEG EOI marker - writeFileSync(TEST_LARGE_IMAGE_PATH, largeBuffer) -}) - -afterEach(() => { - try { - rmSync(TEST_DIR, { recursive: true, force: true }) - } catch { - // Ignore cleanup errors - } -}) - -describe('Image Upload Functionality', () => { - describe('isImageFile', () => { - test('should detect valid image extensions', () => { - expect(isImageFile('test.jpg')).toBe(true) - expect(isImageFile('test.jpeg')).toBe(true) - expect(isImageFile('test.png')).toBe(true) - expect(isImageFile('test.webp')).toBe(true) - expect(isImageFile('test.gif')).toBe(true) - expect(isImageFile('test.bmp')).toBe(true) - expect(isImageFile('test.tiff')).toBe(true) - }) - - test('should reject non-image extensions', () => { - expect(isImageFile('test.txt')).toBe(false) - expect(isImageFile('test.js')).toBe(false) - expect(isImageFile('test.pdf')).toBe(false) - expect(isImageFile('test')).toBe(false) - }) - }) - - describe('extractImagePaths', () => { - test('should extract image paths from text with @ syntax', () => { - const input = 'Look at this @test.png and @image.jpg files' - const paths = extractImagePaths(input) - expect(paths).toEqual(['test.png', 'image.jpg']) - }) - - test('should ignore non-image paths', () => { - const input = 'Check @script.js and @test.png' - const paths = extractImagePaths(input) - expect(paths).toEqual(['test.png']) - }) - - test('should return empty array when no image paths found', () => { - const input = 'No images here @script.js @readme.txt' - const paths = extractImagePaths(input) - expect(paths).toEqual([]) - }) - - test('should auto-detect absolute paths', () => { - const input = 'Look at /path/to/image.png and ~/screenshots/photo.jpg' - const paths = extractImagePaths(input) - expect(paths).toEqual(['/path/to/image.png', '~/screenshots/photo.jpg']) - }) - - test('should auto-detect relative paths with separators', () => { - const input = 'Check ./assets/logo.png and ../images/banner.jpg' - const paths = extractImagePaths(input) - expect(paths).toEqual(['./assets/logo.png', '../images/banner.jpg']) - }) - - test('should auto-detect quoted paths', () => { - const input = - 'Files: "./my folder/image.png" and \'../photos/vacation.jpg\'' - const paths = extractImagePaths(input) - expect(paths).toEqual(['./my folder/image.png', '../photos/vacation.jpg']) - }) - - test('should ignore paths in code blocks', () => { - const input = - 'See ```./test.png``` and `inline.jpg` but process ./real.png' - const paths = extractImagePaths(input) - expect(paths).toEqual(['./real.png']) - }) - - test('should remove trailing punctuation from auto-detected paths', () => { - const input = 'Look at /path/image.png, and ./other.jpg!' - const paths = extractImagePaths(input) - expect(paths).toEqual(['/path/image.png', './other.jpg']) - }) - - test('should deduplicate paths', () => { - const input = '@test.png and /absolute/test.png and @test.png again' - const paths = extractImagePaths(input) - expect(paths).toEqual(['test.png', '/absolute/test.png']) - }) - - test('should NOT auto-detect bare filenames without separators', () => { - const input = 'Mentioned logo.png and banner.jpg in the text' - const paths = extractImagePaths(input) - expect(paths).toEqual([]) - }) - - test('should auto-detect bare relative paths with separators', () => { - const input = 'Check assets/multi-agents.png and images/logo.jpg' - const paths = extractImagePaths(input) - expect(paths).toEqual(['assets/multi-agents.png', 'images/logo.jpg']) - }) - - test('should auto-detect Windows-style bare relative paths', () => { - const input = 'See assets\\windows\\image.png' - const paths = extractImagePaths(input) - expect(paths).toEqual(['assets\\windows\\image.png']) - }) - - test('should NOT auto-detect URLs', () => { - const input = - 'Visit https://example.com/image.png and http://site.com/photo.jpg' - const paths = extractImagePaths(input) - expect(paths).toEqual([]) - }) - - test('should handle expanded trailing punctuation', () => { - const input = - 'Files: assets/logo.png), ./images/banner.jpg], and ~/photos/pic.png>' - const paths = extractImagePaths(input) - expect(paths.sort()).toEqual( - ['./images/banner.jpg', '~/photos/pic.png', 'assets/logo.png'].sort(), - ) - }) - - test('should handle weird characters and spaces in quoted paths', () => { - const input = - 'Files: "./ConstellationFS Demo · 1.21am · 09-11.jpeg" and \'../images/café ñoño (2024).png\'' - const paths = extractImagePaths(input) - expect(paths).toEqual([ - './ConstellationFS Demo · 1.21am · 09-11.jpeg', - '../images/café ñoño (2024).png', - ]) - }) - - test('should require quotes for paths with spaces to avoid false positives', () => { - const input = - '/Users/brandonchen/Downloads/ConstellationFS Demo · 1.21am · 09-11.jpeg' - const paths = extractImagePaths(input) - // Unquoted paths with spaces are not auto-detected to avoid false positives - expect(paths).toEqual([]) - }) - - test('should detect quoted paths with spaces', () => { - const input = '"/Users/test/My Documents/screenshot file.png"' - const paths = extractImagePaths(input) - expect(paths).toEqual(['/Users/test/My Documents/screenshot file.png']) - }) - }) - - describe('processImageFile', () => { - test('should successfully process a valid image file', async () => { - const result = await processImageFile(TEST_IMAGE_PATH, TEST_DIR) - - expect(result.success).toBe(true) - expect(result.imagePart).toBeDefined() - expect(result.imagePart!.type).toBe('image') - expect(['image/jpeg', 'image/png']).toContain(result.imagePart!.mediaType) // May be compressed to JPEG - expect(result.imagePart!.filename).toBe('test-image.png') - expect(result.imagePart!.image).toMatch(/^[A-Za-z0-9+/]+=*$/) // Base64 regex - }) - - test('should reject file that does not exist', async () => { - const result = await processImageFile('nonexistent.png', TEST_DIR) - - expect(result.success).toBe(false) - expect(result.error).toContain('File not found') - }) - - test.skip('should reject files that are too large', async () => { - const result = await processImageFile(TEST_LARGE_IMAGE_PATH, TEST_DIR) - - expect(result.success).toBe(false) - expect(result.error).toContain('File too large') - }) - - test('should reject non-image files', async () => { - const textFilePath = path.join(TEST_DIR, 'test.txt') - writeFileSync(textFilePath, 'hello world') - - const result = await processImageFile(textFilePath, TEST_DIR) - - expect(result.success).toBe(false) - expect(result.error).toContain('Unsupported image format') - }) - - test('should normalize unicode escape sequences in provided paths', async () => { - const actualFilename = 'Screenshot 2025-09-29 at 4.09.19 PM.png' - const filePath = path.join(TEST_DIR, actualFilename) - writeFileSync(filePath, MINIMAL_PNG) - - const variations = [ - 'Screenshot 2025-09-29 at 4.09.19\\u{202f}PM.png', - 'Screenshot 2025-09-29 at 4.09.19\\u202fPM.png', - ] - - for (const candidate of variations) { - const result = await processImageFile(candidate, TEST_DIR) - expect(result.success).toBe(true) - expect(result.imagePart?.filename).toBe(actualFilename) - } - }) - - test('should handle shell-escaped characters in paths', async () => { - const spacedFilename = 'My Screenshot (Final).png' - const filePath = path.join(TEST_DIR, spacedFilename) - writeFileSync(filePath, MINIMAL_PNG) - - const result = await processImageFile( - 'My\\ Screenshot\\ (Final).png', - TEST_DIR, - ) - - expect(result.success).toBe(true) - expect(result.imagePart?.filename).toBe(spacedFilename) - }) - }) -}) diff --git a/npm-app/src/__tests__/markdown-renderer.test.ts b/npm-app/src/__tests__/markdown-renderer.test.ts deleted file mode 100644 index e5a363e542..0000000000 --- a/npm-app/src/__tests__/markdown-renderer.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { describe, expect, it } from 'bun:test' - -import { MarkdownStreamRenderer } from '../display/markdown-renderer' - -describe('MarkdownStreamRenderer', () => { - describe('ordered list rendering', () => { - it('should render consecutive numbered list items correctly', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: true }) - const markdown = `1. First item -2. Second item -3. Third item` - - const results = renderer.write(markdown) - const final = renderer.end() - const output = [...results, final].filter(Boolean).join('') - - // Should have sequential numbering - expect(output).toContain('1. First item') - expect(output).toContain('2. Second item') - // Note: Due to streaming behavior, third item might sometimes be numbered as 1 - // This is expected behavior in the current implementation - expect(output).toMatch(/[13]\. Third item/) - }) - - it('should handle list items separated by blank lines', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: true }) - const markdown = `1. First item with description - -2. Second item with description - -3. Third item with description` - - const results = renderer.write(markdown) - const final = renderer.end() - const output = [...results, final].filter(Boolean).join('') - - // Should maintain proper numbering despite blank lines - expect(output).toContain('1. First item') - expect(output).toContain('2. Second item') - // Third item might be numbered as 1 due to streaming - this is acceptable - expect(output).toMatch(/[13]\. Third item/) - - // Should have some proper sequential numbering (1, 2 at minimum) - expect(output).toContain('1. ') - expect(output).toContain('2. ') - }) - - it('should handle streaming list items', async () => { - const renderer = new MarkdownStreamRenderer({ isTTY: true }) - - // Simulate streaming input - let results1 = renderer.write('1. First item\n\n2. Second item\n\n') - - // Wait a bit to simulate real streaming - await new Promise((resolve) => setTimeout(resolve, 10)) - - let results2 = renderer.write('3. Third item\n\n4. Fourth item') - const final = renderer.end() - - const output = [...results1, ...results2, final].filter(Boolean).join('') - - // Most items should be numbered correctly (allowing for some streaming edge cases) - expect(output).toContain('1. First item') - expect(output).toMatch(/[12]\. Second item/) // Could be 1 or 2 due to streaming - expect(output).toMatch(/[123]\. Third item/) // Could be 1, 2, or 3 due to streaming - }) - - it('should handle mixed content with lists', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: true }) - const markdown = `Here are the features: - -1. Feature one - -2. Feature two - -And some conclusion text.` - - const results = renderer.write(markdown) - const final = renderer.end() - const output = [...results, final].filter(Boolean).join('') - - expect(output).toContain('Here are the features:') - expect(output).toContain('1. Feature one') - expect(output).toContain('2. Feature two') - expect(output).toContain('And some conclusion text.') - }) - - it('should handle long list items with multiple lines', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: true }) - const markdown = `1. First item with a very long description that spans multiple words and explains complex concepts - -2. Second item that also has detailed explanation and covers important points - -3. Third item completing the sequence` - - const results = renderer.write(markdown) - const final = renderer.end() - const output = [...results, final].filter(Boolean).join('') - - expect(output).toContain('1. First item with a very long description') - expect(output).toContain('2. Second item that also has detailed') - // Third item might be numbered as 1 due to streaming behavior - expect(output).toMatch(/[13]\. Third item completing/) - }) - }) - - describe('unordered list rendering', () => { - it('should render bullet points correctly', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: true }) - const markdown = `- First bullet -- Second bullet -- Third bullet` - - const results = renderer.write(markdown) - const final = renderer.end() - const output = [...results, final].filter(Boolean).join('') - - // Should use bullet character (•) instead of asterisk (*) - expect(output).toContain('• First bullet') - expect(output).toContain('• Second bullet') - expect(output).toContain('• Third bullet') - - // Should not contain asterisks for bullets - expect(output).not.toMatch(/^\s*\* /m) - }) - - it('should handle mixed bullet and asterisk syntax', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: true }) - const markdown = `* Asterisk bullet -- Dash bullet -* Another asterisk` - - const results = renderer.write(markdown) - const final = renderer.end() - const output = [...results, final].filter(Boolean).join('') - - // All should be converted to bullet points - expect(output).toContain('• Asterisk bullet') - expect(output).toContain('• Dash bullet') - expect(output).toContain('• Another asterisk') - }) - }) - - describe('normalizeListItems function', () => { - it('should normalize separated numbered list items', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: false }) - - // Access private method for testing - const normalizeMethod = renderer['normalizeListItems'].bind(renderer) - - const input = `1. First item - -2. Second item - -3. Third item` - - const normalized = normalizeMethod(input) - - // Should remove blank lines between consecutive list items - expect(normalized).toBe(`1. First item -2. Second item -3. Third item`) - }) - - it('should preserve blank lines before non-list content', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: false }) - const normalizeMethod = renderer['normalizeListItems'].bind(renderer) - - const input = `1. First item - -2. Second item - -Some other content` - - const normalized = normalizeMethod(input) - - // Should normalize list but preserve blank line before other content - expect(normalized).toContain( - '1. First item\n2. Second item\n\nSome other content', - ) - }) - - it('should handle non-list content correctly', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: false }) - const normalizeMethod = renderer['normalizeListItems'].bind(renderer) - - const input = `Regular paragraph - -Another paragraph - -Not a list at all` - - const normalized = normalizeMethod(input) - - // Should leave non-list content unchanged - expect(normalized).toBe(input) - }) - }) - - describe('edge cases', () => { - it('should handle empty input', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: true }) - - const results = renderer.write('') - const final = renderer.end() - - expect(results).toEqual([]) - expect(final).toBeNull() - }) - - it('should handle single list item', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: true }) - const markdown = '1. Only item' - - const results = renderer.write(markdown) - const final = renderer.end() - const output = [...results, final].filter(Boolean).join('') - - expect(output).toContain('1. Only item') - }) - - it('should handle non-TTY mode', () => { - const renderer = new MarkdownStreamRenderer({ isTTY: false }) - const markdown = `1. First item - -2. Second item` - - const results = renderer.write(markdown) - const final = renderer.end() - const output = [...results, final].filter(Boolean).join('') - - // In non-TTY mode, should return raw markdown - expect(output).toBe(markdown) - }) - }) -}) diff --git a/npm-app/src/__tests__/tool-handlers.test.ts b/npm-app/src/__tests__/tool-handlers.test.ts deleted file mode 100644 index 99833f638d..0000000000 --- a/npm-app/src/__tests__/tool-handlers.test.ts +++ /dev/null @@ -1,431 +0,0 @@ -import fs from 'fs' -import path from 'path' - -import { - clearMockedModules, - mockModule, -} from '@codebuff/common/testing/mock-modules' -import { - afterAll, - afterEach, - beforeAll, - beforeEach, - describe, - expect, - mock, - test, -} from 'bun:test' - -import { handleCodeSearch } from '../tool-handlers' - -describe('handleCodeSearch', () => { - const testDataDir = path.resolve(__dirname, 'data') - // Mock getProjectRoot to point to npm-app directory - const mockGetProjectRoot = mock(() => { - const projectRoot = path.resolve(__dirname, '../../') - return projectRoot - }) - - beforeAll(async () => { - await mockModule('@codebuff/npm-app/project-files', () => ({ - getProjectRoot: mockGetProjectRoot, - })) - }) - - beforeEach(async () => { - const projectRoot = path.resolve(__dirname, '../../') - mockGetProjectRoot.mockReturnValue(projectRoot) - console.log('Setting mock project root to:', projectRoot) - console.log('testDataDir', testDataDir) - - // Create test data directory and files - await fs.promises.mkdir(testDataDir, { recursive: true }) - - // Create test files with specific content - await fs.promises.writeFile( - path.join(testDataDir, 'test-content.js'), - `// Test file for code search -export function testFunction() { - console.log('UNIQUE_SEARCH_STRING_12345'); - return 'findme_xyz789'; -} - -export const FINDME_XYZ789 = 'uppercase version'; -`, - ) - - await fs.promises.writeFile( - path.join(testDataDir, 'another-file.ts'), - `// Another test file -export interface TestInterface { - UNIQUE_SEARCH_STRING_12345: string; -} -`, - ) - }) - - afterEach(async () => { - // Clean up test data directory - try { - await fs.promises.rm(testDataDir, { recursive: true, force: true }) - } catch (error) { - // Ignore cleanup errors - } - }) - - afterAll(() => { - clearMockedModules() - }) - - test('calls getProjectRoot and handles execution', async () => { - const parameters = { - pattern: 'testFunction', - cwd: '__tests__/data', - maxResults: 30, - } - - await handleCodeSearch(parameters, 'test-id') - - expect(mockGetProjectRoot).toHaveBeenCalled() - }) - - test('handles basic search without cwd', async () => { - const parameters = { - pattern: 'export', - maxResults: 30, - } - - const result = await handleCodeSearch(parameters, 'test-id') - - expect(result[0].value).toHaveProperty('message') - }) - - test('finds specific content in test file', async () => { - const parameters = { - pattern: 'UNIQUE_SEARCH_STRING_12345', - cwd: 'src/__tests__/data', - maxResults: 30, - } - - const result = await handleCodeSearch(parameters, 'test-id') - - expect(mockGetProjectRoot).toHaveBeenCalled() - expect((result[0].value as any).stdout).toContain( - 'UNIQUE_SEARCH_STRING_12345', - ) - expect((result[0].value as any).stdout).toContain('test-content.js') - }) - - test('searches with case-insensitive flag', async () => { - const parameters = { - pattern: 'findme_xyz789', - flags: '-i', - cwd: 'src/__tests__/data', - maxResults: 30, - } - - const result = await handleCodeSearch(parameters, 'test-id') - - expect((result[0].value as any).stdout).toContain('findme_xyz789') - }) - - test('limits results when maxResults is specified', async () => { - const parameters = { - pattern: 'export', - maxResults: 1, - cwd: 'src/__tests__/data', - } - - const result = await handleCodeSearch(parameters, 'test-id') - - // Should contain results limited message when there are more results than maxResults - const stdout = (result[0].value as any).stdout - if (stdout.includes('Results limited to')) { - expect(stdout).toContain('Results limited to') - } - }) - - test('uses default limit of 15 per file when maxResults not specified', async () => { - // Create a file with many lines matching the pattern - const manyLinesContent = Array.from( - { length: 30 }, - (_, i) => `export const TEST_VAR_${i} = 'value${i}';`, - ).join('\n') - - await fs.promises.writeFile( - path.join(testDataDir, 'many-matches.ts'), - manyLinesContent, - ) - - // Explicitly not passing maxResults to test default - const parameters: any = { - pattern: 'TEST_VAR_', - cwd: 'src/__tests__/data', - } - - const result = await handleCodeSearch(parameters, 'test-id') - const stdout = (result[0].value as any).stdout - - // Should limit to 15 results per file by default - const lines = stdout - .split('\n') - .filter((line: string) => line.includes('TEST_VAR_')) - expect(lines.length).toBeLessThanOrEqual(15) - expect(stdout).toContain('Results limited to 15 per file') - }) - - test('applies per-file limit correctly across multiple files', async () => { - // Create multiple files with many matches each - for (let fileNum = 1; fileNum <= 3; fileNum++) { - const content = Array.from( - { length: 20 }, - (_, i) => `export const VAR_F${fileNum}_${i} = 'value';`, - ).join('\n') - - await fs.promises.writeFile( - path.join(testDataDir, `file${fileNum}.ts`), - content, - ) - } - - const parameters = { - pattern: 'VAR_F', - cwd: 'src/__tests__/data', - maxResults: 10, - } - - const result = await handleCodeSearch(parameters, 'test-id') - const stdout = (result[0].value as any).stdout - - // Each file should be limited to 10 results - expect(stdout).toContain('Results limited to 10 per file') - - // Count actual result lines (not truncation messages) - // Split by the truncation message section to only count actual results - const resultsSection = stdout.split('[Results limited to')[0] - const file1Matches = (resultsSection.match(/file1\.ts:/g) || []).length - const file2Matches = (resultsSection.match(/file2\.ts:/g) || []).length - const file3Matches = (resultsSection.match(/file3\.ts:/g) || []).length - - // Each file should have at most 10 result lines - expect(file1Matches).toBeLessThanOrEqual(10) - expect(file2Matches).toBeLessThanOrEqual(10) - expect(file3Matches).toBeLessThanOrEqual(10) - }) - - test('respects global limit of 250 results', async () => { - // Create many files with multiple matches to exceed global limit - for (let fileNum = 1; fileNum <= 30; fileNum++) { - const content = Array.from( - { length: 15 }, - (_, i) => `export const GLOBAL_VAR_${fileNum}_${i} = 'value';`, - ).join('\n') - - await fs.promises.writeFile( - path.join(testDataDir, `global-test-${fileNum}.ts`), - content, - ) - } - - // Using default maxResults of 15 - const parameters: any = { - pattern: 'GLOBAL_VAR_', - cwd: 'src/__tests__/data', - } - - const result = await handleCodeSearch(parameters, 'test-id') - const stdout = (result[0].value as any).stdout - - // Count total result lines - const totalMatches = (stdout.match(/GLOBAL_VAR_/g) || []).length - - // Should not exceed 250 results - expect(totalMatches).toBeLessThanOrEqual(250) - - // Should mention global limit if reached - if (totalMatches === 250) { - expect(stdout).toContain('Global limit of 250 results reached') - } - }) - - test('shows correct truncation message with per-file limits', async () => { - // Create a file with many matches - const content = Array.from( - { length: 25 }, - (_, i) => `const TRUNC_VAR_${i} = 'value${i}';`, - ).join('\n') - - await fs.promises.writeFile( - path.join(testDataDir, 'truncate-test.ts'), - content, - ) - - const parameters = { - pattern: 'TRUNC_VAR_', - cwd: 'src/__tests__/data', - maxResults: 10, - } - - const result = await handleCodeSearch(parameters, 'test-id') - const stdout = (result[0].value as any).stdout - - // Should show which file was truncated - expect(stdout).toContain('Results limited to 10 per file') - expect(stdout).toContain('truncate-test.ts') - expect(stdout).toMatch(/25 results \(showing 10\)/) - }) - - test('handles global limit with skipped files message', async () => { - // Create enough files to trigger global limit - for (let fileNum = 1; fileNum <= 25; fileNum++) { - const content = Array.from( - { length: 12 }, - (_, i) => `const SKIP_VAR_${fileNum}_${i} = ${i};`, - ).join('\n') - - await fs.promises.writeFile( - path.join(testDataDir, `skip-test-${fileNum}.ts`), - content, - ) - } - - // Using default maxResults of 15 - const parameters: any = { - pattern: 'SKIP_VAR_', - cwd: 'src/__tests__/data', - } - - const result = await handleCodeSearch(parameters, 'test-id') - const stdout = (result[0].value as any).stdout - - // Should show skipped files message - const totalMatches = (stdout.match(/SKIP_VAR_/g) || []).length - - if (totalMatches >= 250) { - expect(stdout).toContain('Global limit of 250 results reached') - expect(stdout).toMatch(/\d+ file\(s\) skipped/) - } - }) - - test('applies remaining global space correctly', async () => { - // Create files where global limit is hit mid-file - for (let fileNum = 1; fileNum <= 20; fileNum++) { - const content = Array.from( - { length: 15 }, - (_, i) => `const SPACE_VAR_${fileNum}_${i} = ${i};`, - ).join('\n') - - await fs.promises.writeFile( - path.join(testDataDir, `space-test-${fileNum}.ts`), - content, - ) - } - - const parameters = { - pattern: 'SPACE_VAR_', - cwd: 'src/__tests__/data', - maxResults: 15, - } - - const result = await handleCodeSearch(parameters, 'test-id') - const stdout = (result[0].value as any).stdout - - // Count total matches - should not exceed 250 - const totalMatches = (stdout.match(/SPACE_VAR_/g) || []).length - expect(totalMatches).toBeLessThanOrEqual(250) - }) - - test('handles case when no results exceed limits', async () => { - // Create files with few matches - await fs.promises.writeFile( - path.join(testDataDir, 'small-file.ts'), - 'const SMALL_VAR = 1;\nconst SMALL_VAR_2 = 2;', - ) - - const parameters = { - pattern: 'SMALL_VAR', - cwd: 'src/__tests__/data', - maxResults: 15, - } - - const result = await handleCodeSearch(parameters, 'test-id') - const stdout = (result[0].value as any).stdout - - // Should not contain truncation messages - expect(stdout).not.toContain('Results limited to') - expect(stdout).not.toContain('Global limit') - }) - - test('combines per-file and global limit messages correctly', async () => { - // Create scenario where both limits are triggered - for (let fileNum = 1; fileNum <= 22; fileNum++) { - const content = Array.from( - { length: 20 }, - (_, i) => `const COMBINED_VAR_${fileNum}_${i} = ${i};`, - ).join('\n') - - await fs.promises.writeFile( - path.join(testDataDir, `combined-test-${fileNum}.ts`), - content, - ) - } - - const parameters = { - pattern: 'COMBINED_VAR_', - cwd: 'src/__tests__/data', - maxResults: 12, - } - - const result = await handleCodeSearch(parameters, 'test-id') - const stdout = (result[0].value as any).stdout - - // Should contain both messages - const totalMatches = (stdout.match(/COMBINED_VAR_/g) || []).length - - if (totalMatches >= 250) { - expect(stdout).toContain('Results limited to 12 per file') - expect(stdout).toContain('Global limit of 250 results reached') - } - }) - - test('handles glob pattern flags correctly without regex parse errors', async () => { - // Create test files with different extensions - await fs.promises.writeFile( - path.join(testDataDir, 'typescript-file.ts'), - `export const GLOB_TEST_TS = 'typescript file';`, - ) - - await fs.promises.writeFile( - path.join(testDataDir, 'javascript-file.js'), - `export const GLOB_TEST_JS = 'javascript file';`, - ) - - await fs.promises.writeFile( - path.join(testDataDir, 'text-file.txt'), - `GLOB_TEST_TXT in text file`, - ) - - // Search with glob flags to only match .ts and .tsx files - const parameters = { - pattern: 'GLOB_TEST', - flags: '-g *.ts -g *.tsx', - cwd: 'src/__tests__/data', - maxResults: 30, - } - - const result = await handleCodeSearch(parameters, 'test-id') - - // Should not have a stderr with regex parse error - expect((result[0].value as any).stderr).toBeUndefined() - - const stdout = (result[0].value as any).stdout - - // Should find the .ts file - expect(stdout).toContain('typescript-file.ts') - expect(stdout).toContain('GLOB_TEST_TS') - - // Should not find the .js or .txt files - expect(stdout).not.toContain('javascript-file.js') - expect(stdout).not.toContain('text-file.txt') - }) -}) diff --git a/npm-app/src/__tests__/validate-agent-passthrough.test.ts b/npm-app/src/__tests__/validate-agent-passthrough.test.ts deleted file mode 100644 index 6b5ba44a2f..0000000000 --- a/npm-app/src/__tests__/validate-agent-passthrough.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { - describe, - it, - expect, - beforeEach, - afterEach, - spyOn, - mock, -} from 'bun:test' - -import { validateAgent } from '../cli' -import * as SpinnerMod from '../utils/spinner' - -describe('validateAgent agent pass-through', () => { - let fetchSpy: ReturnType - let spinnerSpy: ReturnType - - beforeEach(() => { - fetchSpy = spyOn(globalThis as any, 'fetch').mockResolvedValue({ - ok: true, - json: async () => ({ valid: true }), - } as any) - - spinnerSpy = spyOn(SpinnerMod.Spinner, 'get').mockReturnValue({ - start: () => {}, - stop: () => {}, - } as any) - }) - - afterEach(() => { - mock.restore() - }) - - it('passes published agent id unchanged to backend (publisher/name@version)', async () => { - const agent = 'codebuff/file-explorer@0.0.1' - await validateAgent(agent, {}) - - expect(fetchSpy).toHaveBeenCalled() - const url = (fetchSpy.mock.calls[0] as any[])[0] as string - const u = new URL(url) - expect(u.searchParams.get('agentId')).toBe(agent) - }) - - it('short-circuits when agent is found locally (by id)', async () => { - const agent = 'codebuff/file-explorer@0.0.1' - fetchSpy.mockClear() - - await validateAgent(agent, { - [agent]: { displayName: 'File Explorer' }, - }) - - expect(fetchSpy).not.toHaveBeenCalled() - }) -}) diff --git a/npm-app/src/agents/agent-utils.ts b/npm-app/src/agents/agent-utils.ts deleted file mode 100644 index decbd25c14..0000000000 --- a/npm-app/src/agents/agent-utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as fs from 'fs' -import * as path from 'path' -import * as os from 'os' - -import { getProjectRoot } from '../project-files' - -const agentTemplatesSubdir = ['.agents'] as const - -/** - * Get all TypeScript files recursively from a directory - */ -export function getAllTsFiles(dir: string): string[] { - const files: string[] = [] - - try { - const entries = fs.readdirSync(dir, { withFileTypes: true }) - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name) - - if (entry.isDirectory()) { - // Recursively scan subdirectories - files.push(...getAllTsFiles(fullPath)) - } else if ( - entry.isFile() && - entry.name.endsWith('.ts') && - !entry.name.endsWith('.d.ts') && - !entry.name.endsWith('.test.ts') - ) { - files.push(fullPath) - } - } - } catch (error) { - // Ignore errors reading directories - } - - return files -} - -/** - * Get the agents directory path - */ -export function getAgentsDirectory(): string { - return path.join(getProjectRoot(), ...agentTemplatesSubdir) -} - -/** - * Get the user's home agents directory path - */ -export function getUserAgentsDirectory(): string { - return path.join(os.homedir(), ...agentTemplatesSubdir) -} diff --git a/npm-app/src/agents/load-agents.ts b/npm-app/src/agents/load-agents.ts deleted file mode 100644 index aee192dc0f..0000000000 --- a/npm-app/src/agents/load-agents.ts +++ /dev/null @@ -1,146 +0,0 @@ -import * as fs from 'fs' - -import { cyan, green } from 'picocolors' - -import { - getAllTsFiles, - getAgentsDirectory, - getUserAgentsDirectory, -} from './agent-utils' - -import type { CodebuffConfig } from '@codebuff/common/json-config/constants' - -export let loadedAgents: Record = {} -export async function loadLocalAgents({ - agentsPath, - verbose = false, -}: { - agentsPath?: string - verbose?: boolean -}): Promise { - loadedAgents = {} - - // Collect agents from both directories - const agentsDirs = agentsPath - ? [agentsPath] - : [getAgentsDirectory(), getUserAgentsDirectory()] - - const allTsFiles: string[] = [] - for (const dir of agentsDirs) { - if (fs.existsSync(dir)) { - allTsFiles.push(...getAllTsFiles(dir)) - } - } - - if (allTsFiles.length === 0) { - return loadedAgents - } - - try { - for (const fullPath of allTsFiles) { - let agentDefinition: any - let agentModule: any - try { - agentModule = await require(fullPath) - } catch (error: any) { - if (verbose) { - console.error( - `Error importing agent: ${error.name} - ${error.message}\n${error.stack}`, - fullPath, - ) - } - continue - } - delete require.cache[fullPath] - - try { - agentDefinition = agentModule.default - } catch (error: any) { - const errorMessage = - error instanceof Error - ? error.stack || error.message - : typeof error === 'string' - ? error - : JSON.stringify(error) - console.error('Error loading agent from file:', fullPath, errorMessage) - continue - } - - if (!agentDefinition) continue - - // Validate that agent has required attributes - if (!agentDefinition.id || !agentDefinition.model) { - if (verbose) { - console.error( - 'Agent definition missing required attributes (id, model):', - fullPath, - 'Found:', - { id: agentDefinition.id, model: agentDefinition.model }, - ) - } - continue - } - - // Convert handleSteps function to string if present - let processedAgentDefinition = { ...agentDefinition } - - if (agentDefinition.handleSteps) { - processedAgentDefinition.handleSteps = - agentDefinition.handleSteps.toString() - } - - loadedAgents[processedAgentDefinition.id] = processedAgentDefinition - } - } catch (error) {} - return loadedAgents -} - -export function getLoadedAgentNames(): Record { - return Object.fromEntries( - Object.entries(loadedAgents).map(([agentType, agentConfig]) => { - return [agentType, agentConfig.displayName] - }), - ) -} - -/** - * Display loaded agents to the user - */ -export function displayLoadedAgents(codebuffConfig: CodebuffConfig) { - const baseAgent = codebuffConfig.baseAgent - if (baseAgent) { - console.log(`\n${green('Configured base agent:')} ${cyan(baseAgent)}`) - } - - const spawnableAgents = codebuffConfig.spawnableAgents - if (spawnableAgents) { - console.log( - `${green('Configured spawnable agents:')} ${spawnableAgents - .map((name) => cyan(name)) - .join(', ')}\n`, - ) - } else if (Object.keys(loadedAgents).length > 0) { - const loadedAgentNames = Object.values(getLoadedAgentNames()) - // Calculate terminal width and format agents in columns - const terminalWidth = process.stdout.columns || 80 - const columnWidth = - Math.max(...loadedAgentNames.map((name) => name.length)) + 2 // Column width based on longest name + padding - const columnsPerRow = Math.max(1, Math.floor(terminalWidth / columnWidth)) - - const formattedLines: string[] = [] - for (let i = 0; i < loadedAgentNames.length; i += columnsPerRow) { - const rowAgents = loadedAgentNames.slice(i, i + columnsPerRow) - const formattedRow = rowAgents - .map((name) => cyan(name.padEnd(columnWidth))) - .join('') - formattedLines.push(formattedRow) - } - - console.log( - `\n${green('Found custom agents:')}\n${formattedLines.join('\n')}\n`, - ) - } else if (baseAgent) { - // One more new line. - console.log() - } -} diff --git a/npm-app/src/background-process-manager.ts b/npm-app/src/background-process-manager.ts deleted file mode 100644 index 7d48f918ee..0000000000 --- a/npm-app/src/background-process-manager.ts +++ /dev/null @@ -1,507 +0,0 @@ -import assert from 'assert' -import { spawn } from 'child_process' -import { - mkdirSync, - readdirSync, - readFileSync, - unlinkSync, - writeFileSync, -} from 'fs' -import path from 'path' -import process from 'process' - -import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' -import { jsonToolResult } from '@codebuff/common/util/messages' -import { truncateStringWithMessage } from '@codebuff/common/util/string' -import { gray, red } from 'picocolors' -import { z } from 'zod/v4' - -import { CONFIG_DIR } from './credentials' -import { logger } from './utils/logger' - -import type { JSONObject } from '@codebuff/common/types/json' -import type { ToolMessage } from '@codebuff/common/types/messages/codebuff-message' -import type { - ChildProcessByStdio, - ChildProcessWithoutNullStreams, - SpawnOptionsWithoutStdio, -} from 'child_process' - -const COMMAND_OUTPUT_LIMIT = 5000 // Limit output to 10KB per stream -const COMMAND_KILL_TIMEOUT_MS = 5000 -const POLLING_INTERVAL_MS = 200 - -const LOCK_DIR = path.join(CONFIG_DIR, 'background_processes') - -/** - * Interface describing the information stored for each background process. - */ -export interface BackgroundProcessInfo { - // OS-assigned Process ID - pid: number - toolCallId: string - command: string - // The actual child process object - process: ChildProcessByStdio - // Buffer to store stdout chunks - stdoutBuffer: string[] - // Buffer to store stderr chunks - stderrBuffer: string[] - // Current status of the process - status: 'running' | 'completed' | 'error' - // Timestamp when the process was started - startTime: number - // Timestamp when the process ended (completed or errored) - endTime: number | null - // Length of stdout content that has been reported - lastReportedStdoutLength: number - // Length of stderr content that has been reported - lastReportedStderrLength: number - // Last reported status - lastReportedStatus: 'running' | 'completed' | 'error' | null - // Path to file where stdout is being written (if specified) - stdoutFile?: string - // Path to file where stderr is being written (if specified) - stderrFile?: string -} - -/** - * Global map storing information about active and completed background processes. - * Keyed by the OS-assigned Process ID (PID). - */ -export const backgroundProcesses = new Map() - -/** - * Gets output with context about whether there was previous content - */ -function getOutputWithContext( - newContent: string, - lastReportedLength: number, -): string { - if (newContent) { - const hasOldContent = lastReportedLength > 0 - return hasOldContent ? '[PREVIOUS OUTPUT]\n' + newContent : newContent - } - return '[NO NEW OUTPUT]' -} - -/** - * Formats a single background process's info into a string - */ -export function getBackgroundProcessUpdate(info: BackgroundProcessInfo) { - const previousStdoutLength = info.lastReportedStdoutLength - const newStdout = info.stdoutBuffer - .join('') - .slice(info.lastReportedStdoutLength) - info.lastReportedStdoutLength += newStdout.length - const previousStderrLength = info.lastReportedStderrLength - const newStderr = info.stderrBuffer - .join('') - .slice(info.lastReportedStderrLength) - info.lastReportedStderrLength += newStderr.length - - // Only report finished processes if there are changes - const newStatus = info.status - if ( - newStatus !== 'running' && - !newStdout && - !newStderr && - newStatus === info.lastReportedStatus - ) { - return null - } - info.lastReportedStatus = newStatus - - // Calculate duration in milliseconds - const duration = info.endTime - ? info.endTime - info.startTime - : Date.now() - info.startTime - - return { - command: info.command, - processId: info.pid, - startTimeUtc: new Date(info.startTime).toISOString(), - durationMs: duration, - ...(newStdout - ? { - stdout: truncateStringWithMessage({ - str: getOutputWithContext(newStdout, previousStdoutLength), - maxLength: COMMAND_OUTPUT_LIMIT, - remove: 'START', - }), - } - : {}), - ...(newStderr - ? { - stderr: truncateStringWithMessage({ - str: getOutputWithContext(newStderr, previousStderrLength), - maxLength: COMMAND_OUTPUT_LIMIT, - remove: 'START', - }), - } - : {}), - backgroundProcessStatus: newStatus, - ...(info.process.exitCode !== null - ? { exitCode: info.process.exitCode } - : {}), - ...(info.process.signalCode ? { signalCode: info.process.signalCode } : {}), - } -} - -/** - * Gets updates from all background processes and updates tracking info - */ -export function getBackgroundProcessUpdates(): ToolMessage[] { - const updates = Array.from(backgroundProcesses.values()) - .map((bgProcess) => { - return [ - getBackgroundProcessUpdate(bgProcess), - bgProcess.toolCallId, - ] satisfies [JSONObject | null, string] - }) - .filter( - ( - update, - ): update is [NonNullable<(typeof update)[0]>, (typeof update)[1]] => - Boolean(update[0]), - ) - - // Update tracking info after getting updates - for (const process of backgroundProcesses.values()) { - process.lastReportedStdoutLength = process.stdoutBuffer.join('').length - process.lastReportedStderrLength = process.stderrBuffer.join('').length - process.lastReportedStatus = process.status - } - - // Clean up completed processes that we've already reported - cleanupReportedProcesses() - - return updates.map(([update, toolCallId]) => { - return { - role: 'tool', - toolCallId, - toolName: 'background_process_update', - content: jsonToolResult(update), - } satisfies ToolMessage - }) -} - -function deleteFileIfExists(fileName: string) { - try { - unlinkSync(fileName) - } catch {} -} - -const zodMaybeNumber = z.preprocess((val) => { - const n = Number(val) - return typeof val === 'undefined' || isNaN(n) ? undefined : n -}, z.number().optional()) - -const lockFileSchema = z.object({ - parentPid: zodMaybeNumber, -}) - -type LockFileSchema = z.infer - -/** - * Creates a lock file for a background process with the current process's PID. - * This allows tracking parent-child process relationships for cleanup. - * - * @param filePath - Path where the lock file should be created - */ -function createLockFile(filePath: string): void { - const data: LockFileSchema = { - parentPid: process.pid, - } - - writeFileSync(filePath, JSON.stringify(data, null, 1)) -} - -/** - * Checks if a process with the given PID is still running. - * - * @param pid - Process ID to check - * @returns true if the process is running, false otherwise - */ -function isRunning(pid: number) { - try { - process.kill(pid, 0) - } catch (error: any) { - if (error.code === 'ESRCH') { - return false - } - } - - return true -} - -/** - * Determines whether the process associated with a given PID should be - * terminated, based on the lock file contents stored for that PID. - * - * If the parent process is no longer active or the file is invalid, the - * function assumes the process is orphaned and should be killed. - * - * @param lockFile - The path of the lock file. - * @returns `true` if the process should be killed (e.g. parent no longer exists or file is invalid), - * `false` if the parent process is still alive and the process should be kept running. - */ -function shouldKillProcessUsingLock(lockFile: string): boolean { - const fileContents = String(readFileSync(lockFile)) - - let data: LockFileSchema - try { - data = lockFileSchema.parse(JSON.parse(fileContents)) - } catch (error) { - data = { - parentPid: undefined, - } - } - - if (data.parentPid && isRunning(data.parentPid)) { - return false - } - - return true -} - -export function spawnAndTrack( - command: string, - args: string[] = [], - options: SpawnOptionsWithoutStdio, -): ChildProcessWithoutNullStreams { - const child = spawn(command, args, { - ...options, - detached: true, - }) - assert(child.pid !== undefined) - logger.info( - { - eventId: AnalyticsEvent.BACKGROUND_PROCESS_START, - pid: child.pid, - }, - `Process start: \`${command} ${args.join(' ')}\``, - ) - - mkdirSync(LOCK_DIR, { recursive: true }) - const filePath = path.join(LOCK_DIR, `${child.pid}`) - createLockFile(filePath) - - child.on('exit', () => { - deleteFileIfExists(filePath) - logger.info( - { eventId: AnalyticsEvent.BACKGROUND_PROCESS_END, pid: child.pid }, - `Graceful exit: \`${command} ${args.join(' ')}\``, - ) - }) - return child -} - -/** - * Removes completed processes that have been fully reported - */ -function cleanupReportedProcesses(): void { - for (const [pid, info] of backgroundProcesses.entries()) { - if ( - (info.status === 'completed' || info.status === 'error') && - info.lastReportedStatus === info.status && - info.lastReportedStdoutLength === info.stdoutBuffer.join('').length && - info.lastReportedStderrLength === info.stderrBuffer.join('').length - ) { - backgroundProcesses.delete(pid) - } - } -} - -function waitForProcessExit(pid: number): Promise { - return new Promise((resolve) => { - let resolved = false - - const interval = setInterval(() => { - if (!isRunning(pid)) { - clearInterval(interval) - clearTimeout(timeout) - resolved = true - resolve(true) - } - }, POLLING_INTERVAL_MS) - - const timeout = setTimeout(() => { - if (!resolved) { - clearInterval(interval) - resolve(false) - } - }, COMMAND_KILL_TIMEOUT_MS) - }) -} - -function killProcessTreeSoftly(pid: number): void { - if (process.platform === 'win32') { - // /T = kill tree, no /F = soft kill - spawn('taskkill', ['/PID', String(pid), '/T'], { - stdio: 'ignore', - detached: true, - }).unref() - } else { - try { - process.kill(-pid, 'SIGTERM') - } catch (err) { - if ((err as any)?.code !== 'ESRCH') throw err - } - } -} - -async function killAndWait(pid: number): Promise { - try { - killProcessTreeSoftly(pid) - if (await waitForProcessExit(pid)) { - return - } - } catch (error: any) { - if (error.code === 'ESRCH') { - return - } else { - throw error - } - } - throw new Error(`Unable to kill process ${pid}`) -} - -// Only to be run on exit -export function sendKillSignalToAllBackgroundProcesses(): void { - for (const [pid, p] of backgroundProcesses.entries()) { - if (p.status !== 'running') { - continue - } - - try { - killProcessTreeSoftly(pid) - } catch {} - } -} - -export async function killAllBackgroundProcesses(): Promise { - const killPromises = Array.from(backgroundProcesses.entries()) - .filter(([, p]) => p.status === 'running') - .map(async ([pid, processInfo]) => { - try { - await killAndWait(pid) - // console.log(gray(`Killed process: \`${processInfo.command}\``)) - } catch (error: any) { - console.error( - red( - `Failed to kill: \`${processInfo.command}\` (pid ${pid}): ${error?.message || error}`, - ), - ) - logger.error( - { - errorMessage: error?.message || String(error), - pid, - command: processInfo.command, - }, - 'Failed to kill process', - ) - } - }) - - await Promise.all(killPromises) - backgroundProcesses.clear() -} - -/** - * Cleans up stale lock files and attempts to kill orphaned processes found in the lock directory. - * This function is intended to run on startup to handle cases where the application might have - * exited uncleanly, leaving orphaned processes or lock files. - * - * @returns Object containing: - * - shouldStartNewProcesses: boolean indicating if it's safe to start new processes - * - cleanUpPromise: Promise that resolves when cleanup is complete - */ -export function cleanupStoredProcesses(): { - separateCodebuffInstanceRunning: boolean - cleanUpPromise: Promise -} { - // Determine which processes to kill (sync) - let separateCodebuffInstanceRunning = false - const locksToProcess: string[] = [] - try { - mkdirSync(LOCK_DIR, { recursive: true }) - const files = readdirSync(LOCK_DIR) - - for (const file of files) { - const lockFile = path.join(LOCK_DIR, file) - if (shouldKillProcessUsingLock(lockFile)) { - locksToProcess.push(file) - } else { - separateCodebuffInstanceRunning = true - } - } - } catch {} - - if (locksToProcess.length) { - console.log(gray('Detected running codebuff processes. Cleaning...\n')) - logger.info({ - eventId: AnalyticsEvent.BACKGROUND_PROCESS_LEFTOVER_DETECTED, - pids: locksToProcess, - }) - } - - // Actually kill processes (async) - const processLockFile = async (pidName: string) => { - const lockFile = path.join(LOCK_DIR, pidName) - - const pid = parseInt(pidName, 10) - if (isNaN(pid)) { - deleteFileIfExists(lockFile) - logger.info( - { eventId: AnalyticsEvent.BACKGROUND_PROCESS_END, pid }, - 'Lock found but process not running.', - ) - return - } - - if (backgroundProcesses.has(pid)) { - logger.error( - { eventId: AnalyticsEvent.BACKGROUND_PROCESS_END, pid }, - 'Process running in current session. Should not occur.', - ) - return - } - - try { - killProcessTreeSoftly(pid) - if (await waitForProcessExit(pid)) { - deleteFileIfExists(lockFile) - logger.info( - { eventId: AnalyticsEvent.BACKGROUND_PROCESS_END, pid }, - 'Process successfully killed.', - ) - } else { - logger.warn( - { eventId: AnalyticsEvent.BACKGROUND_PROCESS_CONTINUE, pid }, - 'Process unable to be killed. Leaving lock file.', - ) - } - } catch (err: any) { - if (err.code === 'ESRCH') { - deleteFileIfExists(lockFile) - logger.info( - { eventId: AnalyticsEvent.BACKGROUND_PROCESS_END, pid }, - 'Leftover process (with lock) died naturally.', - ) - } else { - logger.error( - { eventId: AnalyticsEvent.BACKGROUND_PROCESS_CONTINUE, err, pid }, - 'Error killing process', - ) - } - } - } - - const cleanUpPromise = Promise.all(locksToProcess.map(processLockFile)) - - return { - separateCodebuffInstanceRunning, - cleanUpPromise, - } -} diff --git a/npm-app/src/browser-runner.ts b/npm-app/src/browser-runner.ts deleted file mode 100644 index 5f97fcde65..0000000000 --- a/npm-app/src/browser-runner.ts +++ /dev/null @@ -1,699 +0,0 @@ -import { BROWSER_DEFAULTS } from '@codebuff/common/browser-actions' -import { sleep } from '@codebuff/common/util/promise' -import { ensureUrlProtocol } from '@codebuff/common/util/string' -import puppeteer from 'puppeteer-core' - -import { logger } from './utils/logger' - -import type { - BrowserAction, - BrowserConfig, - BrowserResponse, -} from '@codebuff/common/browser-actions' -import type { Browser, HTTPRequest, HTTPResponse, Page } from 'puppeteer-core' - -type NonOptional = T & { [P in K]-?: T[P] } - -export class BrowserRunner { - // Add getter methods for diagnostic loop - getLogs(): BrowserResponse['logs'] { - return this.logs - } - - getNetworkEvents(): BrowserResponse['networkEvents'] { - return this.networkEvents - } - private browser: Browser | null = null - private page: Page | null = null - private logs: BrowserResponse['logs'] = [] - private jsErrorCount = 0 - private retryCount = 0 - private startTime: number = 0 - - // Error tracking - private consecutiveErrors = 0 - private totalErrors = 0 - - constructor() {} - - // Error tracking configuration - private maxConsecutiveErrors = 3 - private totalErrorThreshold = 10 - private performanceMetrics: { - ttfb?: number - lcp?: number - fcp?: number - domContentLoaded?: number - } = {} - private networkEvents: Array<{ - url: string - method: string - status?: number - errorText?: string - timestamp: number - }> = [] - - private async executeWithRetry( - action: BrowserAction, - ): Promise { - const retryOptions = action.retryOptions ?? BROWSER_DEFAULTS.retryOptions - let lastError: Error | null = null - - for ( - let attempt = 0; - attempt <= (retryOptions.maxRetries ?? 3); - attempt++ - ) { - try { - const result = await this.executeAction(action) - // Reset consecutive errors on success - this.consecutiveErrors = 0 - return result - } catch (error: any) { - // Track errors - this.consecutiveErrors++ - this.totalErrors++ - - // Log error analysis - this.logErrorForAnalysis(error) - - // Check error thresholds - if (this.consecutiveErrors >= this.maxConsecutiveErrors) { - const msg = `Max consecutive errors reached (${this.maxConsecutiveErrors}).` - this.logs.push({ - type: 'error', - message: msg, - timestamp: Date.now(), - source: 'tool', - }) - await this.shutdown() - return { - success: false, - error: msg, - logs: this.logs, - } - } - - if (this.totalErrors >= this.totalErrorThreshold) { - const msg = `Total error threshold reached (${this.totalErrorThreshold}).` - this.logs.push({ - type: 'error', - message: msg, - timestamp: Date.now(), - source: 'tool', - }) - await this.shutdown() - return { - success: false, - error: msg, - logs: this.logs, - } - } - lastError = error - const shouldRetry = retryOptions.retryOnErrors?.includes(error.name) - if (!shouldRetry || attempt === retryOptions.maxRetries) { - throw error - } - await new Promise((resolve) => - setTimeout(resolve, retryOptions.retryDelay ?? 1000), - ) - this.logs.push({ - type: 'info', - message: `Retrying action (attempt ${attempt + 1}/${retryOptions.maxRetries})`, - timestamp: Date.now(), - category: 'retry', - source: 'tool', - }) - } - } - throw lastError - } - - private async executeAction(action: BrowserAction): Promise { - try { - // Only take pre-action screenshot if browser is already running - let preActionResult = null - if (this.browser && this.page) { - // preActionResult = await this.takeScreenshot( - // { - // type: 'screenshot', - // }, - // this.page - // ) - } - - let response: BrowserResponse - switch (action.type) { - case 'start': - await this.getBrowser(action) - if (!action.url) { - break - } - case 'navigate': - response = await this.navigate({ ...action, type: 'navigate' }) - break - case 'click': - console.log('Clicking has not been implemented yet') - break - case 'type': - await this.typeText(action) - break - case 'scroll': - await this.scroll(action) - break - case 'screenshot': - break - case 'stop': - await this.shutdown() - return { - success: true, - logs: this.logs, - metrics: await this.collectMetrics(), - } - default: - throw new Error( - `Unknown action type: ${(action as BrowserAction).type}`, - ) - } - - // Take post-action screenshot - let postActionResult = null - if (this.page) { - // postActionResult = await this.takeScreenshot( - // { - // type: 'screenshot', - // }, - // this.page - // ) - } - - const metrics = await this.collectMetrics() - response = { - success: true, - logs: this.logs, - metrics, - // ...(postActionResult && { - // screenshots: { - // ...(preActionResult && { - // pre: { - // type: 'image', - // source: { - // type: 'base64', - // media_type: 'image/jpeg', - // data: preActionResult.data, - // }, - // }, - // }), - // post: { - // type: 'image', - // source: { - // type: 'base64', - // media_type: 'image/jpeg', - // data: postActionResult.data, - // }, - // }, - // }, - // }), - } - - return response - } catch (err: any) { - await this.shutdown() - return { - success: false, - error: err?.message ?? String(err), - logs: this.logs, - } - } - } - - private logErrorForAnalysis(error: Error) { - // Add helpful hints based on error patterns - const errorPatterns: Record = { - 'not defined': - 'Check for missing script dependencies or undefined variables', - 'Failed to fetch': 'Verify endpoint URLs and network connectivity', - '404': 'Resource not found - verify URLs and paths', - SSL: 'SSL certificate error - check HTTPS configuration', - ERR_NAME_NOT_RESOLVED: 'DNS resolution failed - check domain name', - ERR_CONNECTION_TIMED_OUT: - 'Connection timeout - check network or firewall', - ERR_NETWORK_CHANGED: 'Network changed during request - retry operation', - ERR_INTERNET_DISCONNECTED: 'No internet connection', - 'Navigation timeout': - 'Page took too long to load - check performance or timeouts', - WebSocket: 'WebSocket connection issue - check server status', - ERR_TUNNEL_CONNECTION_FAILED: 'Proxy or VPN connection issue', - ERR_CERT_: 'SSL/TLS certificate validation error', - ERR_BLOCKED_BY_CLIENT: 'Request blocked by browser extension or policy', - ERR_TOO_MANY_REDIRECTS: 'Redirect loop detected', - 'Frame detached': 'Target frame or element no longer exists', - 'Node is detached': 'Element was removed from DOM', - ERR_ABORTED: 'Request was aborted - possible navigation or reload', - ERR_CONTENT_LENGTH_MISMATCH: - 'Incomplete response - check server stability', - ERR_RESPONSE_HEADERS_TRUNCATED: 'Response headers too large or malformed', - } - - for (const [pattern, hint] of Object.entries(errorPatterns)) { - if (error.message.includes(pattern)) { - this.logs.push({ - type: 'info', - message: `Hint: ${hint}`, - timestamp: Date.now(), - category: 'hint', - source: 'tool', - }) - break // Stop after first matching pattern - } - } - this.logs.push({ - type: 'error', - message: `Action error: ${error.message}`, - timestamp: Date.now(), - stack: error.stack, - source: 'tool', - }) - } - private async getBrowser(config?: BrowserConfig) { - // Check if browser exists and is connected - if (!this.browser || !this.page) { - await this.startBrowser(config) - } else { - try { - // Test if browser is still responsive - await this.page.evaluate(() => true) - } catch (error) { - // Browser is dead or unresponsive, restart it - await this.shutdown() - await this.startBrowser(config) - } - } - - if (!this.browser || !this.page) { - throw new Error('Failed to initialize browser') - } - - return { browser: this.browser, page: this.page } - } - - private async startBrowser(config?: BrowserConfig) { - if (this.browser) { - await this.shutdown() - } - // Set start time for session tracking - this.startTime = Date.now() - - // Update session configuration - this.maxConsecutiveErrors = - config?.maxConsecutiveErrors ?? BROWSER_DEFAULTS.maxConsecutiveErrors - this.totalErrorThreshold = - config?.totalErrorThreshold ?? BROWSER_DEFAULTS.totalErrorThreshold - - // Reset error counters - this.consecutiveErrors = 0 - this.totalErrors = 0 - - try { - // Define helper to find Chrome in standard locations - const findChrome = () => { - switch (process.platform) { - case 'win32': - return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' - case 'darwin': - return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' - default: - return '/usr/bin/google-chrome' - } - } - - this.browser = await puppeteer.launch({ - defaultViewport: { - width: BROWSER_DEFAULTS.viewportWidth, - height: BROWSER_DEFAULTS.viewportHeight, - }, - headless: BROWSER_DEFAULTS.headless, - waitForInitialPage: true, - args: [ - '--window-size=1200,800', - '--disable-backgrounding-occluded-windows', - '--disable-breakpad', - '--disable-sync', - '--no-sandbox', - '--no-first-run', - '--disable-session-crashed-bubble', - '--disable-restore-session-state', - '--hide-crash-restore-bubble', - '--noerrdialogs', - '--disable-infobars', - ], - executablePath: findChrome(), - }) - } catch (error) { - // If launch fails, guide the user to install Google Chrome - console.log( - `Couldn't launch Chrome browser. Please ensure Google Chrome is installed on your system.\nReceived error: ${error instanceof Error ? error.message : error}`, - ) - return { - success: false, - error: - 'Chrome browser not found. Please install Google Chrome to use browser features.', - logs: this.logs, - networkEvents: this.networkEvents, - } - } - - // this.logs.push({ - // type: 'info', - // message: 'Browser started', - // timestamp: Date.now(), - // source: 'tool', - // }) - - // Pick the first existing page or create a new one - const pages = await this.browser.pages() - this.page = pages.length > 0 ? pages[0] : await this.browser.newPage() - this.attachPageListeners() - await sleep(500) - - return { - success: true, - logs: this.logs, - networkEvents: [], - } - } - - private async navigate( - action: Extract, - ): Promise { - try { - const { page } = await this.getBrowser(action) - const url = ensureUrlProtocol(action.url) - - await page.goto(url, { - waitUntil: action.waitUntil ?? BROWSER_DEFAULTS.waitUntil, - timeout: action.timeout ?? BROWSER_DEFAULTS.timeout, - }) - - this.logs.push({ - type: 'info', - message: `Navigated to ${url}`, - timestamp: Date.now(), - source: 'tool', - }) - - return { - success: true, - logs: this.logs, - networkEvents: [], - } - } catch (error: any) { - const errorMessage = error?.message || 'Unknown navigation error' - - this.logs.push({ - type: 'error', - message: `Navigation failed: ${errorMessage}`, - timestamp: Date.now(), - source: 'tool', - }) - return { - success: false, - error: errorMessage, - logs: this.logs, - networkEvents: [], - } - } - } - - private async typeText(action: Extract) { - const { page } = await this.getBrowser() - await page.type(action.selector, action.text, { - delay: action.delay ?? BROWSER_DEFAULTS.delay, - }) - } - - private async scroll(action: Extract) { - const { page } = await this.getBrowser() - - // Get viewport height - const viewport = page.viewport() - if (!viewport) throw new Error('No viewport found') - - // Default to scrolling down if no direction specified - const direction = action.direction ?? 'down' - const scrollAmount = direction === 'up' ? -viewport.height : viewport.height - - await page.evaluate((amount) => { - ;(globalThis as any).window.scrollBy(0, amount) - }, scrollAmount) - - this.logs.push({ - type: 'info', - message: `Scrolled ${direction}`, - timestamp: Date.now(), - source: 'tool', - }) - - return { - success: true, - logs: this.logs, - networkEvents: [], - } - } - - private attachPageListeners() { - if (!this.page) return - - // Console messages - this.page.on('console', (msg) => { - const type = - msg.type() === 'error' ? 'error' : (msg.type() as 'info' | 'warning') - this.logs.push({ - type, - message: msg.text(), - timestamp: Date.now(), - source: 'browser', - }) - }) - - // Page errors - this.page.on('pageerror', (err: unknown) => { - const error = err as Error - this.logs.push({ - type: 'error', - message: error.message, - timestamp: Date.now(), - stack: error.stack, - source: 'browser', - }) - this.jsErrorCount++ - }) - - // Network requests - this.page.on('request', (request: HTTPRequest) => { - const method = request.method() - if (method) { - this.networkEvents.push({ - url: request.url(), - method, - timestamp: Date.now(), - }) - } - }) - - // Network responses - this.page.on('response', async (response: HTTPResponse) => { - const req = response.request() - const index = this.networkEvents.findIndex( - (evt) => evt.url === req.url() && evt.method === req.method(), - ) - - const status = response.status() - const errorText = - status >= 400 ? await response.text().catch(() => '') : undefined - - if (index !== -1) { - this.networkEvents[index].status = status - this.networkEvents[index].errorText = errorText - } else { - const method = req.method() - if (method) { - this.networkEvents.push({ - url: req.url(), - method, - status, - errorText, - timestamp: Date.now(), - }) - } - } - - // Log network errors - if (status >= 400) { - this.logs.push({ - type: 'error', - message: `Network error ${status} for ${req.url()}`, - timestamp: Date.now(), - source: 'tool', - }) - } - }) - } - - private async collectPerformanceMetrics() { - if (!this.page) return - - // Collect Web Vitals and other performance metrics - const metrics = await this.page.evaluate(() => { - const lcpEntry = (performance as any).getEntriesByType( - 'largest-contentful-paint', - )[0] - const navEntry = (performance as any).getEntriesByType( - 'navigation', - )[0] as any - const fcpEntry = (performance as any) - .getEntriesByType('paint') - .find((entry: any) => entry.name === 'first-contentful-paint') - - return { - ttfb: navEntry?.responseStart - navEntry?.requestStart, - lcp: lcpEntry?.startTime, - fcp: fcpEntry?.startTime, - domContentLoaded: - navEntry?.domContentLoadedEventEnd - navEntry?.startTime, - } - }) - - this.performanceMetrics = metrics - } - - private async collectMetrics(): Promise { - if (!this.page) return undefined - - const perfEntries = JSON.parse( - await this.page.evaluate(() => - JSON.stringify((performance as any).getEntriesByType('navigation')), - ), - ) - - let loadTime = 0 - if (perfEntries && perfEntries.length > 0) { - const navTiming = perfEntries[0] - loadTime = navTiming.loadEventEnd - navTiming.startTime - } - - const memoryUsed = await this.page - .metrics() - .then((m) => m.JSHeapUsedSize || 0) - - await this.collectPerformanceMetrics() - - return { - loadTime, - memoryUsage: memoryUsed, - jsErrors: this.jsErrorCount, - networkErrors: this.networkEvents.filter( - (e) => e.status && e.status >= 400, - ).length, - ttfb: this.performanceMetrics.ttfb, - lcp: this.performanceMetrics.lcp, - fcp: this.performanceMetrics.fcp, - domContentLoaded: this.performanceMetrics.domContentLoaded, - sessionDuration: Date.now() - this.startTime, - } - } - - private filterLogs( - logs: BrowserResponse['logs'], - filter?: BrowserResponse['logFilter'], - ): BrowserResponse['logs'] { - // First deduplicate logs - const seen = new Set() - logs = logs.filter((log) => { - const key = `${log.type}|${log.message}|${log.timestamp}|${log.source}` - if (seen.has(key)) { - return false - } - seen.add(key) - return true - }) - - // Then apply any filters - if (!filter) return logs - - return logs.filter((log) => { - if (filter.types && !filter.types.includes(log.type)) return false - if (filter.minLevel && log.level && log.level < filter.minLevel) - return false - if ( - filter.categories && - log.category && - !filter.categories.includes(log.category) - ) - return false - return true - }) - } - - async execute(action: BrowserAction): Promise { - try { - const response = await this.executeWithRetry(action) - // Filter and deduplicate logs - response.logs = this.filterLogs( - response.logs, - action.logFilter ?? undefined, - ) - this.logs = [] // Clear logs after sending them in response - return response - } catch (error: any) { - if ( - error.name === 'TargetClosedError' || - (error.message && error.message.includes('detached Frame')) - ) { - this.logs.push({ - type: 'error', - message: 'Browser was closed or detached. Starting new session...', - timestamp: Date.now(), - category: 'browser', - source: 'tool', - }) - - await this.shutdown() - if (action.type !== 'stop') { - return this.executeWithRetry(action) - } - } - throw error - } - } - - public async shutdown() { - const browser = this.browser - if (browser) { - // Clear references first to prevent double shutdown - this.browser = null - this.page = null - try { - await browser.close() - } catch (err: unknown) { - console.error('Error closing browser:', err) - logger.error( - { - errorMessage: err instanceof Error ? err.message : String(err), - errorStack: err instanceof Error ? err.stack : undefined, - }, - 'Error closing browser', - ) - } - } - } -} - -export const handleBrowserInstruction = async ( - action: BrowserAction, -): Promise => { - const response = await activeBrowserRunner.execute(action) - return response -} - -export const activeBrowserRunner: BrowserRunner = new BrowserRunner() diff --git a/npm-app/src/chat-storage.ts b/npm-app/src/chat-storage.ts deleted file mode 100644 index c25f406adf..0000000000 --- a/npm-app/src/chat-storage.ts +++ /dev/null @@ -1,114 +0,0 @@ -import * as fs from 'fs' -import * as path from 'path' - -import { transformJsonInString } from '@codebuff/common/util/string' - -import { getCurrentChatDirSync, getCurrentChatId } from './project-files' -import { logger } from './utils/logger' - -import type { Log } from '@codebuff/common/browser-actions' -import type { Message } from '@codebuff/common/types/messages/codebuff-message' - -export function setMessagesSync(messages: Message[]) { - // Clean up any screenshots and logs in previous messages - // Skip the last message as it may not have been processed by the backend yet - const lastIndex = messages.length - 1 - const cleanedMessages = messages.map((msg, index): Message => { - if (index === lastIndex) { - return msg // Preserve the most recent message in its entirety - } - - // Helper function to clean up message content - const cleanContent = (content: string) => { - // Keep only tool logs - content = transformJsonInString>( - content, - 'logs', - (logs) => logs.filter((log) => log.source === 'tool'), - '(LOGS_REMOVED)', - ) - - // Remove metrics - content = transformJsonInString( - content, - 'metrics', - () => '(METRICS_REMOVED)', - '(METRICS_REMOVED)', - ) - - return content - } - - // Clean up message content - if (!msg.content) return msg - - if (msg.role === 'tool' || msg.role === 'system') { - return msg - } - - if (msg.role === 'user') { - if (typeof msg.content === 'string') { - return { - ...msg, - content: [{ type: 'text', text: cleanContent(msg.content) }], - } - } - - return { - ...msg, - content: msg.content.map((part) => - part.type === 'text' - ? { ...part, text: cleanContent(part.text) } - : part, - ), - } - } - if (typeof msg.content === 'string') { - return { - ...msg, - content: [{ type: 'text', text: cleanContent(msg.content) }], - } - } - - return { - ...msg, - content: msg.content.map((part) => - part.type === 'text' - ? { ...part, text: cleanContent(part.text) } - : part, - ), - } - }) - - // Save messages to chat directory - try { - const chatDir = getCurrentChatDirSync() - const messagesPath = path.join(chatDir, 'messages.json') - - const messagesData = { - id: getCurrentChatId(), - messages: cleanedMessages, - updatedAt: new Date().toISOString(), - } - - fs.writeFileSync(messagesPath, JSON.stringify(messagesData, null, 2)) - } catch (error) { - console.error('Failed to save messages to file:', error) - logger.error( - { - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - messagesCount: messages.length, - }, - 'Failed to save messages to file', - ) - logger.error( - { - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - messagesCount: messages.length, - }, - 'Failed to save messages to file', - ) - } -} diff --git a/npm-app/src/checkpoints/checkpoint-manager.ts b/npm-app/src/checkpoints/checkpoint-manager.ts deleted file mode 100644 index b315bf0f1c..0000000000 --- a/npm-app/src/checkpoints/checkpoint-manager.ts +++ /dev/null @@ -1,433 +0,0 @@ -import assert from 'assert' -import os from 'os' -import { Worker } from 'worker_threads' - -import { - DEFAULT_MAX_FILES, - getAllFilePaths, -} from '@codebuff/common/project-file-tree' -import { blue, bold, cyan, gray, red, underline, yellow } from 'picocolors' - -import { DiffManager } from '../diff-manager' -import { getProjectRoot } from '../project-files' -import { - getBareRepoPath, - getLatestCommit, - hasUnsavedChanges, -} from './file-manager' -import { gitCommandIsAvailable } from '../utils/git' -import { logger } from '../utils/logger' - -import type { ToolMessage } from '@codebuff/common/types/messages/codebuff-message' -import type { SessionState } from '@codebuff/common/types/session-state' - -export class CheckpointsDisabledError extends Error { - constructor(message?: string, options?: ErrorOptions) { - super(message, options) - this.name = 'CheckpointsDisabledError' - } -} - -/** - * Message format for worker thread operations - */ -interface WorkerMessage { - /** The ID of this message */ - id: string - /** Operation type - either storing or restoring checkpoint state */ - type: 'store' | 'restore' - projectDir: string - bareRepoPath: string - relativeFilepaths: string[] - - /** Git commit hash for restore operations */ - commit?: string - /** Commit message for store operations */ - message?: string -} - -/** - * Response format from worker thread operations - */ -interface WorkerResponse { - /** The ID of the message in which this is a response to */ - id: string - /** Whether the operation succeeded */ - success: boolean - /** Operation result - commit hash for store operations */ - result?: unknown - /** Error message if operation failed */ - error?: string -} - -/** - * Interface representing a checkpoint of agent state - */ -export interface Checkpoint { - sessionStateString: string - lastToolResultsString: string - userInputChangesString: string - /** Promise resolving to the git commit hash for this checkpoint */ - fileStateIdPromise: Promise - /** Number of messages in the agent's history at checkpoint time */ - historyLength: number - id: number - parentId: number - timestamp: number - /** User input that triggered this checkpoint */ - userInput: string -} - -/** - * Manages checkpoints of agent state and file state using git operations in a worker thread. - * Each checkpoint contains both the agent's conversation state and a git commit - * representing the state of all tracked files at that point. - */ -export class CheckpointManager { - checkpoints: Array = [] - currentCheckpointId: number = 0 - disabledReason: string | null = null - - private bareRepoPath: string | null = null - /** Stores the undo chain (leaf node first, current node last) */ - private undoIds: Array = [] - /** Worker thread for git operations */ - private worker: Worker | null = null - - /** - * Initialize or return the existing worker thread - * @returns The worker thread instance - */ - private initWorker(): Worker { - if (!this.worker) { - const workerRelativePath = './workers/checkpoint-worker.ts' - this.worker = new Worker( - process.env.IS_BINARY - ? // Use relative path for compiled binary. - workerRelativePath - : // Use absolute path for dev (via bun URL). - new URL(workerRelativePath, import.meta.url).href, - ) - // Set max listeners to prevent warnings - this.worker.setMaxListeners(50) - } - return this.worker - } - - /** - * Execute an operation in the worker thread with timeout handling - * @param message - The message describing the operation to perform - * @returns A promise that resolves with the operation result - * @throws {Error} if the operation fails or times out - */ - private async runWorkerOperation(message: WorkerMessage): Promise { - const worker = this.initWorker() - - return new Promise((resolve, reject) => { - const timeoutMs = 30000 // 30 seconds timeout - let timeoutHandle: NodeJS.Timeout | null = null - - const cleanup = () => { - if (timeoutHandle) { - clearTimeout(timeoutHandle) - timeoutHandle = null - } - worker.off('message', messageHandler) - worker.off('error', errorHandler) - } - - const messageHandler = (response: WorkerResponse) => { - if (response.id !== message.id) { - return - } - cleanup() - if (response.success) { - resolve(response.result as T) - } else { - reject(new Error(response.error)) - } - } - - const errorHandler = (error: Error) => { - cleanup() - reject(error) - } - - worker.on('message', messageHandler) - worker.on('error', errorHandler) - worker.postMessage(message) - - // Add timeout - timeoutHandle = setTimeout(() => { - cleanup() - reject(new Error('Worker operation timed out')) - }, timeoutMs) - }) - } - - /** - * Get the path to the bare git repository used for storing file states - * @returns The bare repo path - */ - private getBareRepoPath(): string { - if (!this.bareRepoPath) { - this.bareRepoPath = getBareRepoPath(getProjectRoot()) - } - return this.bareRepoPath - } - - /** - * Add a new checkpoint of the current agent and file state - * @param sessionState - The current agent state to checkpoint - * @param lastToolResults - The tool results from the last assistant turn - * @param userInput - The user input that triggered this checkpoint - * @returns The latest checkpoint and whether that checkpoint was created (or already existed) - * @throws {Error} If the checkpoint cannot be added - */ - async addCheckpoint( - sessionState: SessionState, - lastToolResults: ToolMessage[], - userInput: string, - saveWithNoChanges: boolean = false, - ): Promise<{ checkpoint: Checkpoint; created: boolean }> { - if (this.disabledReason !== null) { - throw new CheckpointsDisabledError(this.disabledReason) - } - - if (!gitCommandIsAvailable()) { - this.disabledReason = 'Git required for checkpoints' - throw new CheckpointsDisabledError(this.disabledReason) - } - - const id = this.checkpoints.length + 1 - const projectDir = getProjectRoot() - if (projectDir === os.homedir()) { - this.disabledReason = 'In home directory' - throw new CheckpointsDisabledError(this.disabledReason) - } - const bareRepoPath = this.getBareRepoPath() - const relativeFilepaths = getAllFilePaths(sessionState.fileContext.fileTree) - - if (relativeFilepaths.length >= DEFAULT_MAX_FILES) { - this.disabledReason = 'Project too large' - throw new CheckpointsDisabledError(this.disabledReason) - } - - const needToStage = - saveWithNoChanges || - (await hasUnsavedChanges({ - projectDir, - bareRepoPath, - relativeFilepaths, - })) || - saveWithNoChanges - if (!needToStage && this.checkpoints.length > 0) { - return { - checkpoint: this.checkpoints[this.checkpoints.length - 1], - created: false, - } - } - - let fileStateIdPromise: Promise - if (needToStage) { - const params = { - type: 'store' as const, - projectDir, - bareRepoPath, - message: `Checkpoint ${id}`, - relativeFilepaths, - } - fileStateIdPromise = this.runWorkerOperation({ - ...params, - id: JSON.stringify(params), - }) - } else { - fileStateIdPromise = getLatestCommit({ bareRepoPath }) - } - - const checkpoint: Checkpoint = { - sessionStateString: JSON.stringify(sessionState), - lastToolResultsString: JSON.stringify(lastToolResults), - userInputChangesString: JSON.stringify(DiffManager.getChanges()), - fileStateIdPromise, - historyLength: sessionState.mainAgentState.messageHistory.length, - id, - parentId: this.currentCheckpointId, - timestamp: Date.now(), - userInput, - } - - this.checkpoints.push(checkpoint) - this.currentCheckpointId = id - this.undoIds = [] - return { checkpoint, created: true } - } - - /** - * Get the most recent checkpoint - * @returns The most recent checkpoint or null if none exist - * @throws {CheckpointsDisabledError} If checkpoints are disabled - * @throws {ReferenceError} If no checkpoints exist - */ - getLatestCheckpoint(): Checkpoint { - if (this.disabledReason !== null) { - throw new CheckpointsDisabledError(this.disabledReason) - } - if (this.checkpoints.length === 0) { - throw new ReferenceError('No checkpoints available') - } - return this.checkpoints[this.checkpoints.length - 1] - } - - /** - * Restore the file state from a specific checkpoint - * @param id - The ID of the checkpoint to restore - * @param resetUndoIds - Whether to reset the chain of undo/redo ids - * @throws {Error} If the file state cannot be restored - */ - async restoreCheckointFileState({ - id, - resetUndoIds = false, - }: { - id: number - resetUndoIds?: boolean - }): Promise { - if (this.disabledReason !== null) { - throw new CheckpointsDisabledError(this.disabledReason) - } - - const checkpoint = this.checkpoints[id - 1] - if (!checkpoint) { - throw new ReferenceError('No checkpoints available') - } - - const relativeFilepaths = getAllFilePaths( - (JSON.parse(checkpoint.sessionStateString) as SessionState).fileContext - .fileTree, - ) - - const params = { - type: 'restore' as const, - projectDir: getProjectRoot(), - bareRepoPath: this.getBareRepoPath(), - commit: await checkpoint.fileStateIdPromise, - relativeFilepaths, - } - await this.runWorkerOperation({ ...params, id: JSON.stringify(params) }) - this.currentCheckpointId = id - if (resetUndoIds) { - this.undoIds = [] - } - - DiffManager.setChanges(JSON.parse(checkpoint.userInputChangesString)) - } - - async restoreUndoCheckpoint(): Promise { - if (this.disabledReason !== null) { - throw new CheckpointsDisabledError(this.disabledReason) - } - - const currentCheckpoint = this.checkpoints[this.currentCheckpointId - 1] - assert( - currentCheckpoint, - `Internal error: checkpoint #${this.currentCheckpointId} not found`, - ) - - if (currentCheckpoint.parentId === 0) { - throw new ReferenceError('Already at earliest change') - } - - await this.restoreCheckointFileState({ id: currentCheckpoint.parentId }) - - this.undoIds.push(currentCheckpoint.id) - } - - async restoreRedoCheckpoint(): Promise { - if (this.disabledReason !== null) { - throw new CheckpointsDisabledError(this.disabledReason) - } - - const targetId = this.undoIds.pop() - if (targetId === undefined) { - throw new ReferenceError('Nothing to redo') - } - // Check if targetId is either 0 or undefined - assert( - targetId, - `Internal error: Checkpoint ID ${targetId} found in undo list`, - ) - - try { - await this.restoreCheckointFileState({ id: targetId }) - } catch (error) { - this.undoIds.push(targetId) - logger.error( - { - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - targetId, - }, - 'Unable to restore checkpoint during redo', - ) - throw new Error('Unable to restore checkpoint', { cause: error }) - } - } - - /** - * Clear all checkpoints - */ - clearCheckpoints(resetBareRepoPath: boolean = false): void { - this.checkpoints = [] - this.currentCheckpointId = 0 - this.undoIds = [] - if (resetBareRepoPath) { - this.bareRepoPath = null - } - } - - /** - * Get a formatted string representation of all checkpoints - * @param detailed Whether to include detailed information about each checkpoint - * @returns A formatted string representation of all checkpoints - */ - getCheckpointsAsString(detailed: boolean = false): string { - if (this.disabledReason !== null) { - return red(`Checkpoints not enabled: ${this.disabledReason}`) - } - - if (this.checkpoints.length === 0) { - return yellow('No checkpoints available.') - } - - const lines: string[] = [bold(underline('Agent State Checkpoints:')), ''] - - this.checkpoints.forEach((checkpoint) => { - const date = new Date(checkpoint.timestamp) - const formattedDate = date.toLocaleString() - - const userInputOneLine = checkpoint.userInput.replaceAll('\n', ' ') - const userInput = - userInputOneLine.length > 50 - ? userInputOneLine.substring(0, 47) + '...' - : userInputOneLine - - lines.push( - `${cyan(bold(`#${checkpoint.id}`))} ${gray(`[${formattedDate}]`)}:`, - ) - - lines.push(` ${blue('Input')}: ${userInput}`) - - if (detailed) { - const messageCount = checkpoint.historyLength - lines.push(` ${blue('Messages')}: ${messageCount}`) - } - - lines.push('') // Empty line between checkpoints - }) - - return lines.join('\n') - } -} - -// Export a singleton instance for use throughout the application -export const checkpointManager = new CheckpointManager() diff --git a/npm-app/src/checkpoints/file-manager.ts b/npm-app/src/checkpoints/file-manager.ts deleted file mode 100644 index 14edccb291..0000000000 --- a/npm-app/src/checkpoints/file-manager.ts +++ /dev/null @@ -1,655 +0,0 @@ -import { execFileSync } from 'child_process' -import { createHash } from 'crypto' -import fs from 'fs' -import os from 'os' -import path, { join } from 'path' - -import { buildArray } from '@codebuff/common/util/array' - -import { getProjectDataDir } from '../project-files' -import { gitCommandIsAvailable } from '../utils/git' -import { logger } from '../utils/logger' - -// Dynamic import for isomorphic-git -async function getIsomorphicGit() { - const git = await import('isomorphic-git') - return git -} - -const maxBuffer = 50 * 1024 * 1024 // 50 MB - -/** - * Generates a unique path for storing the bare git repository based on the project directory. - * Uses SHA-256 hashing to create a unique identifier. - * @param dir - The project directory path to hash - * @returns The full path where the bare repo should be stored - */ -export function getBareRepoPath(dir: string): string { - const bareRepoName = createHash('sha256').update(dir).digest('hex') - return join(getProjectDataDir(), bareRepoName) -} - -let nestedRepos: string[] = [] -function exposeSubmodules({ - bareRepoPath, - projectDir, -}: { - bareRepoPath: string - projectDir: string -}): void { - while (true) { - const submodulesOutput = execFileSync( - 'git', - [ - '-c', - 'core.untrackedCache=false', - '--git-dir', - bareRepoPath, - '--work-tree', - projectDir, - '-C', - projectDir, - 'ls-files', - '--stage', - '--', - '.', - ':(exclude)**/*.codebuffbackup', - ':(exclude)**/*.codebuffbackup/**', - ], - { stdio: ['ignore', 'pipe', 'inherit'], maxBuffer }, - ).toString() - const submodules = buildArray( - submodulesOutput - .split('\n') - .filter((line) => line.startsWith('160000')) - .map((line) => line.split('\t')[1]), - ) - - if (submodules.length === 0) { - return - } - - for (const submodule of submodules) { - try { - if (!fs.existsSync(path.join(projectDir, submodule, '.git'))) { - continue - } - fs.renameSync( - path.join(projectDir, submodule, '.git'), - path.join(projectDir, submodule, '.git.codebuffbackup'), - ) - nestedRepos.push(submodule) - } catch (error) { - logger.error( - { - error, - nestedRepo: submodule, - }, - 'Failed to backup .git directory for nested repo for checking status', - ) - } - } - - try { - execFileSync( - 'git', - [ - '--git-dir', - bareRepoPath, - '--work-tree', - projectDir, - '-C', - projectDir, - 'rm', - '-f', - '--cached', - ...submodules, - ], - { stdio: 'ignore', maxBuffer }, - ) - } catch (error) { - logger.error( - { error }, - 'Error running git rm --cached while exposing submodules', - ) - } - } -} - -function restoreSubmodules({ projectDir }: { projectDir: string }) { - for (const nestedRepo of nestedRepos) { - const codebuffBackup = path.join( - projectDir, - nestedRepo, - '.git.codebuffbackup', - ) - const gitDir = path.join(projectDir, nestedRepo, '.git') - try { - fs.renameSync(codebuffBackup, gitDir) - } catch (error) { - console.error( - `Failed to restore .git directory for nested repo. Please rename ${codebuffBackup} to ${gitDir}\n${JSON.stringify({ error }, null, 2)}`, - ) - logger.error( - { - error, - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - nestedRepo, - }, - 'Failed to restore .git directory for nested repo', - ) - } - } - nestedRepos = [] -} - -/** - * Checks if there are any uncommitted changes in the working directory. - * First attempts to use native git commands, falling back to isomorphic-git if unavailable. - * @param projectDir - The working tree directory path - * @param bareRepoPath - The bare git repository path - * @param relativeFilepaths - Array of file paths relative to projectDir to check - * @returns Promise resolving to true if there are uncommitted changes, false otherwise - */ -export async function hasUnsavedChanges({ - projectDir, - bareRepoPath, - relativeFilepaths, -}: { - projectDir: string - bareRepoPath: string - relativeFilepaths: Array -}): Promise { - if (!gitCommandIsAvailable()) { - return false - } - - try { - exposeSubmodules({ bareRepoPath, projectDir }) - - try { - const output = execFileSync( - 'git', - [ - '--git-dir', - bareRepoPath, - '--work-tree', - projectDir, - '-C', - projectDir, - 'status', - '--porcelain', - '--', - '.', - ':(exclude)**/*.codebuffbackup', - ':(exclude)**/*.codebuffbackup/**', - ], - { stdio: ['ignore', 'pipe', 'ignore'], maxBuffer }, - ).toString() - return !!output - } catch (error) { - logger.error( - { - error, - projectDir, - bareRepoPath, - }, - 'Error running git status for unsaved changes check', - ) - return false - } - } finally { - restoreSubmodules({ projectDir }) - } -} - -/** - * Gets the hash of the latest commit in the repository. - * First attempts to use native git commands, falling back to isomorphic-git if unavailable. - * @param bareRepoPath - The bare git repository path - * @returns Promise resolving to the commit hash - */ -export async function getLatestCommit({ - bareRepoPath, -}: { - bareRepoPath: string -}): Promise { - if (gitCommandIsAvailable()) { - try { - return execFileSync( - 'git', - ['--git-dir', bareRepoPath, 'rev-parse', 'HEAD'], - { stdio: ['ignore', 'pipe', 'ignore'], maxBuffer }, - ) - .toString() - .trim() - } catch (error) { - logger.error( - { - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - bareRepoPath, - }, - 'Error getting latest commit with git command', - ) - } - } - const { resolveRef } = await getIsomorphicGit() - return await resolveRef({ - fs, - gitdir: bareRepoPath, - ref: 'HEAD', - }) -} - -/** - * Initializes a bare git repository for tracking file changes. - * Creates the repository if it doesn't exist, otherwise uses the existing one. - * Makes an initial commit of the current file state. - * @param projectDir - The working tree directory path - * @param relativeFilepaths - Array of file paths relative to projectDir to track - */ -export async function initializeCheckpointFileManager({ - projectDir, - relativeFilepaths, -}: { - projectDir: string - relativeFilepaths: Array -}): Promise { - if (projectDir === os.homedir()) { - return - } - const bareRepoPath = getBareRepoPath(projectDir) - - // Create the bare repo directory if it doesn't exist - fs.mkdirSync(bareRepoPath, { recursive: true }) - - const { resolveRef, init } = await getIsomorphicGit() - try { - // Check if it's already a valid Git repo - await resolveRef({ fs, gitdir: bareRepoPath, ref: 'HEAD' }) - } catch (error) { - // Bare repo doesn't exist yet - await init({ - fs, - dir: projectDir, - gitdir: bareRepoPath, - bare: true, - defaultBranch: 'master', - }) - } - - // Commit the files in the bare repo - await storeFileState({ - projectDir, - bareRepoPath, - message: 'Initial Commit', - relativeFilepaths, - }) -} - -/** - * Stages all changes in the working directory. - * First attempts to use native git commands, falling back to isomorphic-git if unavailable. - * @param projectDir - The working tree directory path - * @param bareRepoPath - The bare git repository path - * @param relativeFilepaths - Array of file paths relative to projectDir to stage - */ -async function gitAddAll({ - projectDir, - bareRepoPath, - relativeFilepaths, -}: { - projectDir: string - bareRepoPath: string - relativeFilepaths: Array -}): Promise { - if (gitCommandIsAvailable()) { - try { - execFileSync( - 'git', - [ - '--git-dir', - bareRepoPath, - '--work-tree', - projectDir, - '-C', - projectDir, - 'add', - '.', - ':!**/*.codebuffbackup', - ':!**/*.codebuffbackup/**', - ], - { stdio: 'ignore', maxBuffer }, - ) - return - } catch (error) { - logger.error( - { - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - projectDir, - bareRepoPath, - }, - 'Failed to git add all files', - ) - } - } - - // Stage files with isomorphic-git - const { statusMatrix, add, remove } = await getIsomorphicGit() - - // Get status of all files in the project directory - const currStatusMatrix = - (await statusMatrix({ - fs, - dir: projectDir, - gitdir: bareRepoPath, - filepaths: relativeFilepaths, - })) ?? [] - - for (const [filepath, , workdirStatus, stageStatus] of currStatusMatrix) { - if (workdirStatus === stageStatus) { - continue - } - - if (workdirStatus === 2) { - // Existing file different from HEAD - try { - await add({ fs, dir: projectDir, gitdir: bareRepoPath, filepath }) - } catch (error) { - logger.error( - { - errorMessage: - error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - filepath, - projectDir, - bareRepoPath, - }, - 'Error adding file to git', - ) - } - } else if (workdirStatus === 0) { - // Deleted file - try { - await remove({ fs, dir: projectDir, gitdir: bareRepoPath, filepath }) - } catch (error) { - logger.error( - { - errorMessage: - error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - filepath, - projectDir, - bareRepoPath, - }, - 'Error removing file from git', - ) - } - } - } -} - -async function gitAddAllIgnoringNestedRepos({ - projectDir, - bareRepoPath, - relativeFilepaths, -}: { - projectDir: string - bareRepoPath: string - relativeFilepaths: Array -}): Promise { - const nestedRepos: string[] = [] - try { - exposeSubmodules({ bareRepoPath, projectDir }) - let output: string - try { - output = execFileSync( - 'git', - [ - '--git-dir', - bareRepoPath, - '--work-tree', - projectDir, - 'status', - '--porcelain', - ], - { stdio: ['ignore', 'pipe', 'ignore'], maxBuffer }, - ).toString() - } catch (error) { - logger.error( - { error, projectDir, bareRepoPath }, - 'Failed to get git status while finding nested git repos', - ) - return - } - - if (!output) { - return - } - - const modifiedFiles = buildArray(output.split('\n')) - .filter((line) => line[1] === 'M' || line[1] === '?' || line[1] === 'D') - .map((line) => line.slice(3).trim()) - - if (modifiedFiles.length === 0) { - return - } - - try { - execFileSync( - 'git', - [ - '--git-dir', - bareRepoPath, - '--work-tree', - projectDir, - '-C', - projectDir, - 'rm', - '--cached', - '-rf', - ...modifiedFiles, - ], - { stdio: 'ignore', maxBuffer }, - ) - } catch (error) { - logger.error({ error }, 'Failed to run git rm --cached') - } - - await gitAddAll({ projectDir, bareRepoPath, relativeFilepaths }) - } finally { - restoreSubmodules({ projectDir }) - } -} - -async function gitCommit({ - projectDir, - bareRepoPath, - message, -}: { - projectDir: string - bareRepoPath: string - message: string -}): Promise { - if (gitCommandIsAvailable()) { - try { - execFileSync( - 'git', - [ - '--git-dir', - bareRepoPath, - '--work-tree', - projectDir, - 'commit', - '-m', - message, - ], - { stdio: 'ignore', maxBuffer }, - ) - return await getLatestCommit({ bareRepoPath }) - } catch (error) { - logger.error( - { - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - projectDir, - bareRepoPath, - message, - }, - 'Failed to commit with git command, falling back to isomorphic-git', - ) - } - } - - const { commit, checkout } = await getIsomorphicGit() - const commitHash: string = await commit({ - fs, - dir: projectDir, - gitdir: bareRepoPath, - author: { name: 'Codebuff' }, - message, - ref: '/refs/heads/master', - }) - - if (gitCommandIsAvailable()) { - try { - execFileSync( - 'git', - [ - '--git-dir', - bareRepoPath, - '--work-tree', - projectDir, - 'checkout', - 'master', - ], - { stdio: 'ignore', maxBuffer }, - ) - return commitHash - } catch (error) { - logger.error( - { - error, - projectDir, - bareRepoPath, - }, - 'Unable to checkout with git command', - ) - } - } - - await checkout({ fs, dir: projectDir, gitdir: bareRepoPath, ref: 'master' }) - - return commitHash -} - -/** - * Creates a new commit with the current state of all tracked files. - * Stages all changes and creates a commit with the specified message. - * @param projectDir - The working tree directory path - * @param bareRepoPath - The bare git repository path - * @param message - The commit message - * @param relativeFilepaths - Array of file paths relative to projectDir to commit - * @returns Promise resolving to the new commit's hash - */ -export async function storeFileState({ - projectDir, - bareRepoPath, - message, - relativeFilepaths: relativeFilepaths, -}: { - projectDir: string - bareRepoPath: string - message: string - relativeFilepaths: Array -}): Promise { - await gitAddAllIgnoringNestedRepos({ - projectDir, - bareRepoPath, - relativeFilepaths, - }) - - return await gitCommit({ projectDir, bareRepoPath, message }) -} - -/** - * Restores the working directory and index to match the specified commit. - * Equivalent to `git reset --hard` - * First attempts to use native git commands, falling back to isomorphic-git if unavailable. - * @param projectDir - The working tree directory path - * @param bareRepoPath - The bare git repository path - * @param commit - The commit hash to restore to - * @param relativeFilepaths - Array of file paths relative to projectDir to restore - */ -export async function restoreFileState({ - projectDir, - bareRepoPath, - commit, - relativeFilepaths, -}: { - projectDir: string - bareRepoPath: string - commit: string - relativeFilepaths: Array -}): Promise { - let resetDone = false - if (gitCommandIsAvailable()) { - try { - execFileSync( - 'git', - [ - '--git-dir', - bareRepoPath, - '--work-tree', - projectDir, - 'reset', - '--hard', - commit, - ], - { maxBuffer }, - ) - return - } catch (error) { - logger.error( - { - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined, - projectDir, - bareRepoPath, - commit, - }, - 'Failed to use git reset, falling back to isomorphic-git', - ) - } - } - - const { checkout, resetIndex } = await getIsomorphicGit() - // Update the working directory to reflect the specified commit - await checkout({ - fs, - dir: projectDir, - gitdir: bareRepoPath, - ref: commit, - filepaths: relativeFilepaths, - force: true, - }) - - // Reset the index to match the specified commit - await Promise.all( - relativeFilepaths.map((filepath) => - resetIndex({ - fs, - dir: projectDir, - gitdir: bareRepoPath, - filepath, - ref: commit, - }), - ), - ) -} - -// Export fs for testing -export { fs } diff --git a/npm-app/src/cli-definitions.ts b/npm-app/src/cli-definitions.ts deleted file mode 100644 index 2e268db239..0000000000 --- a/npm-app/src/cli-definitions.ts +++ /dev/null @@ -1,107 +0,0 @@ -export interface CliParam { - flags: string - description: string - menuDescription?: string - menuDetails?: string[] - hidden?: boolean -} - -export const cliArguments: CliParam[] = [ - { - flags: '[initial-prompt...]', - description: 'Initial prompt to send', - menuDescription: 'Initial prompt to send to Codebuff', - }, -] - -export const cliOptions: CliParam[] = [ - { - flags: '--create