Skip to content

Latest commit

 

History

History
99 lines (74 loc) · 5.21 KB

File metadata and controls

99 lines (74 loc) · 5.21 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Build and Test Commands

Run all tests:

bun test

Run a single test file:

bun test src/commands/ask.test.ts

Type check (no emit):

npx tsc --noEmit

Run the CLI directly:

bun run src/main.ts <command> [args]

The project has no build scripts in package.json — it runs directly via Bun which handles TypeScript natively. dist/ is gitignored.

Project Architecture

my-cli is a Bun + TypeScript CLI AI assistant. It provides multi-turn LLM conversations with function calling (tool use), session management, MCP tool integration, and file attachments.

Runtime: Bun (not Node.js)

  • Uses Bun.file(), Bun.write(), Bun.spawn() — native Bun APIs
  • Tests use bun:test (describe, test, expect, beforeEach, afterEach)
  • No package.json scripts — run via bun run src/main.ts or bun test

Dependency Injection Pattern for Testing

The askCommand exports factory objects (not bare functions) for all external dependencies. Tests override properties on these factories:

// In ask.ts
export const streamChatFactory = { call: streamChat };
export const storeFactory = { getSession, getOrCreateActiveSession, updateSession, setActiveSessionId };
export const chatWithToolsFactory = { call: streamChatWithTools };
export const executorFactory = { execute: executeUnifiedTool };
export const toolsStoreFactory = { loadTools: getUnifiedToolDefs };

Test example: streamChatFactory.call = async () => ({ reply: 'mock', thinking: '' })

Core Data Flow

  1. Entry: src/main.ts — parses argv, loads config, resolves command via Registry
  2. Registry: src/registry.ts — tree-based command router (supports nested subcommands like session new)
  3. Args: src/args.ts — parses --flag value, --boolean, positional args; file flag aggregates into arrays; --btw skips session save
  4. Config: src/config/loader.ts merges 3 layers: file (~/.config/my-cli/config.json) → env vars (MY_CLI_*) → CLI overrides
  5. Ask flow (src/commands/ask.ts): loads agent.md as system prompt → loads session → applies trimMessages → calls LLM via streamChat (no tools) or streamChatWithTools loop (up to 10 tool call iterations) → renders markdown → saves session

Key Modules

Module Purpose
src/llm/client.ts streamChat() returns {reply, thinking}; streamChatWithTools() returns {reply, thinking, toolCalls}; both use SSE streaming internally via streamChatInternal()
src/tools/store.ts Unified tool system: getUnifiedToolDefs() merges builtins + MCP tools; executeUnifiedTool() routes by source field
src/tools/base.ts ToolExecutor interface: { execute(args): Promise<string> }
src/tools/builtin/ Built-in tools: weather (Open-Meteo API), file (read/write/append)
src/mcp/client.ts Lazy singleton mcporter runtime; discovers tools from enabled servers; naming: servername__toolname
src/session/store.ts Session CRUD; ID format YYYYMMDD-HHmmss-xxxx; files in ~/.config/my-cli/sessions/
src/utils/context.ts trimMessages() uses group-based token counting (preserves assistant+tool pairs); triggers at 80% context, trims to 50%
src/utils/tokenizer.ts tiktoken cl100k_base singleton; must call freeEncoder() after each command
src/utils/file.ts File attachments: images → base64 data URL; text files → inline content

Config Files (all in ~/.config/my-cli/)

File Schema
config.json zod-validated: model, contextWindow, activeSessionId, chatMode, builtinTools record
llm-providers.json LLMConfig: { providers: LLMProvider[], defaultProvider: string }; each provider has models: ModelMap with context limits
agent.md System prompt, generated by my-cli init
mcp-servers.json { mcpServers: Record<string, MCPServerConfig> }; discriminated union: { type: 'local', command: string[] } or { type: 'remote', url: string }

Types

  • ChatRole: 'system' | 'user' | 'assistant' | 'tool'
  • Message.role: adds 'thinking' for reasoning content storage
  • UnifiedTool.source: 'builtin' | 'mcp' — used to route tool execution
  • MCP tool names use servername__toolname format (double underscore separator)

Important Implementation Details

  1. streamChatWithTools returns empty reply on tool_calls (line 271 in llm/client.ts): when finishReason === 'tool_calls', fullReply is reset to '' and reply: '' is returned
  2. trimMessages preserves tool call groups: groupMessages() groups assistant messages with their following tool messages, so trimming never breaks a call/result pair
  3. --btw flag: messages are NOT saved to session when this flag is set
  4. chatMode: 'lite' | 'normal': defined in schema but not actively used in code paths yet
  5. loadConfig() is synchronous (uses readFileSync), while saveConfig() is async (uses writeFileSync wrapped in Promise)
  6. MCP runtime is a lazy singleton in mcp/client.ts (_runtime), closed via closeRuntime() in ask's finally block