├── .dockerignore ├── main.js ├── src ├── tools │ ├── TaskTool │ │ └── constants.ts │ ├── MCPTool │ │ └── prompt.ts │ ├── MemoryReadTool │ │ └── prompt.ts │ ├── MemoryWriteTool │ │ └── prompt.ts │ ├── lsTool │ │ └── prompt.ts │ ├── NotebookReadTool │ │ └── prompt.ts │ ├── GlobTool │ │ └── prompt.ts │ ├── FileWriteTool │ │ └── prompt.ts │ ├── NotebookEditTool │ │ └── prompt.ts │ ├── GrepTool │ │ └── prompt.ts │ ├── FileReadTool │ │ └── prompt.ts │ ├── ThinkTool │ │ ├── prompt.ts │ │ └── ThinkTool.tsx │ ├── BashTool │ │ ├── BashToolResultMessage.tsx │ │ ├── OutputLine.tsx │ │ └── utils.ts │ ├── ArchitectTool │ │ └── prompt.ts │ ├── StickerRequestTool │ │ └── prompt.ts │ ├── FileEditTool │ │ └── utils.ts │ └── MultiEditTool │ │ └── prompt.ts ├── constants │ ├── keys.ts │ ├── figures.ts │ ├── macros.ts │ ├── releaseNotes.ts │ ├── betas.ts │ ├── oauth.ts │ └── product.ts ├── services │ ├── sentry.ts │ ├── notifier.ts │ ├── adapters │ │ └── base.ts │ ├── mcpServerApproval.tsx │ ├── browserMocks.ts │ └── responseStateManager.ts ├── utils │ ├── array.ts │ ├── json.ts │ ├── auth.ts │ ├── http.ts │ ├── browser.ts │ ├── state.ts │ ├── errors.ts │ ├── betas.ts │ ├── unaryLogging.ts │ ├── responseState.ts │ ├── style.ts │ ├── diff.ts │ ├── format.tsx │ ├── imagePaste.ts │ ├── tokens.ts │ ├── user.ts │ ├── execFileNoThrow.ts │ ├── sessionState.ts │ ├── terminal.ts │ ├── globalLogger.ts │ ├── generators.ts │ ├── env.ts │ ├── conversationRecovery.ts │ ├── cleanup.ts │ ├── git.ts │ ├── fileRecoveryCore.ts │ └── agentStorage.ts ├── components │ ├── TodoItem.tsx │ ├── PressEnterToContinue.tsx │ ├── MessageResponse.tsx │ ├── messages │ │ ├── UserToolResultMessage │ │ │ ├── UserToolCanceledMessage.tsx │ │ │ ├── UserToolRejectMessage.tsx │ │ │ ├── UserToolSuccessMessage.tsx │ │ │ ├── UserToolErrorMessage.tsx │ │ │ ├── UserToolResultMessage.tsx │ │ │ └── utils.tsx │ │ ├── AssistantRedactedThinkingMessage.tsx │ │ ├── AssistantBashOutputMessage.tsx │ │ ├── UserBashInputMessage.tsx │ │ ├── UserKodingInputMessage.tsx │ │ ├── UserCommandMessage.tsx │ │ ├── TaskProgressMessage.tsx │ │ ├── AssistantThinkingMessage.tsx │ │ ├── UserPromptMessage.tsx │ │ ├── UserTextMessage.tsx │ │ ├── AssistantLocalCommandOutputMessage.tsx │ │ └── TaskToolMessage.tsx │ ├── AsciiLogo.tsx │ ├── StickerRequestForm.tsx │ ├── FallbackToolUseRejectedMessage.tsx │ ├── Cost.tsx │ ├── permissions │ │ ├── utils.ts │ │ ├── hooks.ts │ │ ├── PermissionRequestTitle.tsx │ │ ├── toolUseOptions.ts │ │ ├── FileEditPermissionRequest │ │ │ └── FileEditToolDiff.tsx │ │ └── FileWritePermissionRequest │ │ │ └── FileWriteToolDiff.tsx │ ├── SentryErrorBoundary.ts │ ├── CustomSelect │ │ ├── use-select.ts │ │ ├── theme.ts │ │ ├── option-map.ts │ │ └── select-option.tsx │ ├── TokenWarning.tsx │ ├── MCPServerDialogCopy.tsx │ ├── ToolUseLoader.tsx │ ├── HighlightedCode.tsx │ ├── Link.tsx │ ├── CostThresholdDialog.tsx │ ├── binary-feedback │ │ └── BinaryFeedback.tsx │ └── FileEditToolUpdatedMessage.tsx ├── hooks │ ├── useLogStartupTime.ts │ ├── useLogMessages.ts │ ├── useInterval.ts │ ├── useExitOnCtrlCD.ts │ ├── useCancelRequest.ts │ ├── useDoublePress.ts │ ├── useTerminalSize.ts │ ├── usePermissionRequestLogging.ts │ ├── useArrowKeyHistory.ts │ ├── useApiKeyVerification.ts │ └── useNotifyAfterTimeout.ts ├── commands │ ├── cost.ts │ ├── config.tsx │ ├── bug.tsx │ ├── help.tsx │ ├── modelstatus.tsx │ ├── doctor.ts │ ├── onboarding.tsx │ ├── resume.tsx │ ├── logout.tsx │ ├── release-notes.ts │ ├── listen.ts │ ├── clear.ts │ ├── mcp.ts │ ├── model.tsx │ ├── init.ts │ ├── login.tsx │ ├── review.ts │ ├── approvedTools.ts │ ├── pr_comments.ts │ └── refreshCommands.ts ├── history.ts ├── messages.ts ├── types │ ├── logs.ts │ ├── conversation.ts │ ├── modelCapabilities.ts │ ├── RequestContext.ts │ └── notebook.ts ├── screens │ ├── LogList.tsx │ └── ResumeConversation.tsx ├── cost-tracker.ts └── Tool.ts ├── yoga.wasm ├── .prettierrc ├── .claude └── agents │ ├── test-agent.md │ ├── a-agent-like-linus-keep-it-sim.md │ ├── test-writer.md │ ├── simplicity-auditor.md │ └── dao-qi-harmony-designer.md ├── .prettierignore ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── npm-publish.yml │ ├── version-bump.yml │ └── release.yml ├── .kode └── agents │ ├── search-specialist.md │ ├── code-writer.md │ ├── dao-qi-harmony-designer.md │ ├── test-writer.md │ └── docs-writer.md ├── docs └── PUBLISH.md ├── scripts ├── prepublish-check.js ├── postinstall.js └── publish-workaround.js ├── Dockerfile ├── CONTRIBUTING.md ├── tsconfig.json └── test └── customCommands.test.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | Testing file 2 2 | -------------------------------------------------------------------------------- /src/tools/TaskTool/constants.ts: -------------------------------------------------------------------------------- 1 | export const TOOL_NAME = 'Task' 2 | -------------------------------------------------------------------------------- /yoga.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/Kode/main/yoga.wasm -------------------------------------------------------------------------------- /src/constants/keys.ts: -------------------------------------------------------------------------------- 1 | export const SENTRY_DSN = '' 2 | 3 | export const STATSIG_CLIENT_KEY = '' 4 | -------------------------------------------------------------------------------- /src/services/sentry.ts: -------------------------------------------------------------------------------- 1 | export function initSentry(): void {} 2 | 3 | export async function captureException(error: unknown): Promise {} 4 | -------------------------------------------------------------------------------- /src/tools/MCPTool/prompt.ts: -------------------------------------------------------------------------------- 1 | // Actual prompt and description are overridden in mcpClient.ts 2 | export const PROMPT = '' 3 | export const DESCRIPTION = '' 4 | -------------------------------------------------------------------------------- /src/tools/MemoryReadTool/prompt.ts: -------------------------------------------------------------------------------- 1 | // Actual prompt and description are overridden in mcpClient.ts 2 | export const PROMPT = '' 3 | export const DESCRIPTION = '' 4 | -------------------------------------------------------------------------------- /src/tools/MemoryWriteTool/prompt.ts: -------------------------------------------------------------------------------- 1 | // Actual prompt and description are overridden in mcpClient.ts 2 | export const PROMPT = '' 3 | export const DESCRIPTION = '' 4 | -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | export function intersperse(as: A[], separator: (index: number) => A): A[] { 2 | return as.flatMap((a, i) => (i ? [separator(i), a] : [a])) 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "tabWidth": 2, 4 | "printWidth": 80, 5 | "singleQuote": true, 6 | "arrowParens": "avoid", 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /.claude/agents/test-agent.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: test-agent 3 | description: Test agent for validation 4 | tools: '*' 5 | model: claude-3-5-sonnet-20241022 6 | color: cyan 7 | --- 8 | 9 | You are a test agent. -------------------------------------------------------------------------------- /src/constants/figures.ts: -------------------------------------------------------------------------------- 1 | import { env } from '../utils/env' 2 | 3 | // The former is better vertically aligned, but isn't usually supported on Windows/Linux 4 | export const BLACK_CIRCLE = env.platform === 'macos' ? '⏺' : '●' 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | pnpm-lock.yaml 4 | 5 | # Build outputs 6 | cli.mjs 7 | dist/ 8 | build/ 9 | out/ 10 | 11 | # Misc 12 | .git/ 13 | .DS_Store 14 | coverage/ 15 | .idea/ 16 | .vscode/ 17 | *.log -------------------------------------------------------------------------------- /src/tools/lsTool/prompt.ts: -------------------------------------------------------------------------------- 1 | export const DESCRIPTION = 2 | 'Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You should generally prefer the Glob and Grep tools, if you know which directories to search.' 3 | -------------------------------------------------------------------------------- /src/utils/json.ts: -------------------------------------------------------------------------------- 1 | import { logError } from './log' 2 | 3 | export function safeParseJSON(json: string | null | undefined): unknown { 4 | if (!json) { 5 | return null 6 | } 7 | try { 8 | return JSON.parse(json) 9 | } catch (e) { 10 | logError(e) 11 | return null 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/constants/macros.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../../package.json' 2 | 3 | export const MACRO = { 4 | VERSION: version, 5 | README_URL: 'https://docs.anthropic.com/s/claude-code', 6 | PACKAGE_URL: '@shareai-lab/kode', 7 | ISSUES_EXPLAINER: 'report the issue at https://github.com/shareAI-lab/kode/issues', 8 | } 9 | -------------------------------------------------------------------------------- /src/components/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface TodoItemProps { 4 | // Define props as needed 5 | children?: React.ReactNode 6 | } 7 | 8 | export const TodoItem: React.FC = ({ children }) => { 9 | // Minimal component implementation 10 | return <>{children} 11 | } -------------------------------------------------------------------------------- /src/constants/releaseNotes.ts: -------------------------------------------------------------------------------- 1 | // Release notes for each version 2 | // Don't add more than 3 for any version, since these show up in the UI upon launch. 3 | export const RELEASE_NOTES: Record = { 4 | '0.1.178': [ 5 | "New release notes now show you what's changed since you last launched", 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /src/constants/betas.ts: -------------------------------------------------------------------------------- 1 | export const GATE_TOKEN_EFFICIENT_TOOLS = 'tengu-token-efficient-tools' 2 | export const BETA_HEADER_TOKEN_EFFICIENT_TOOLS = 3 | 'token-efficient-tools-2024-12-11' 4 | export const GATE_USE_EXTERNAL_UPDATER = 'tengu-use-external-updater' 5 | export const CLAUDE_CODE_20250219_BETA_HEADER = 'claude-code-20250219' 6 | -------------------------------------------------------------------------------- /src/components/PressEnterToContinue.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { getTheme } from '../utils/theme' 3 | import { Text } from 'ink' 4 | 5 | export function PressEnterToContinue(): React.ReactNode { 6 | return ( 7 | 8 | Press Enter to continue… 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /.claude/agents/a-agent-like-linus-keep-it-sim.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: a-agent-like-linus-keep-it-sim 3 | description: "Use this agent when you need assistance with: a agent like linus, keep it simaple and stupid " 4 | model: glm-4.5 5 | color: pink 6 | --- 7 | 8 | You are a specialized assistant focused on helping with a agent like linus, keep it simaple and stupid . Provide expert-level assistance in this domain. -------------------------------------------------------------------------------- /src/hooks/useLogStartupTime.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { logEvent } from '../services/statsig' 3 | 4 | export function useLogStartupTime(): void { 5 | useEffect(() => { 6 | const startupTimeMs = Math.round(process.uptime() * 1000) 7 | logEvent('tengu_timer', { 8 | event: 'startup', 9 | durationMs: String(startupTimeMs), 10 | }) 11 | }, []) 12 | } 13 | -------------------------------------------------------------------------------- /src/components/MessageResponse.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from 'ink' 2 | import * as React from 'react' 3 | 4 | type Props = { 5 | children: React.ReactNode 6 | } 7 | 8 | export function MessageResponse({ children }: Props): React.ReactNode { 9 | return ( 10 | 11 | {' '}⎿   12 | {children} 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'ink' 2 | import * as React from 'react' 3 | import { getTheme } from '../../../utils/theme' 4 | 5 | export function UserToolCanceledMessage(): React.ReactNode { 6 | return ( 7 | 8 |   ⎿   9 | Interrupted by user 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { USE_BEDROCK, USE_VERTEX } from './model' 2 | import { getGlobalConfig } from './config' 3 | 4 | export function isAnthropicAuthEnabled(): boolean { 5 | return false 6 | // return !(USE_BEDROCK || USE_VERTEX) 7 | } 8 | 9 | export function isLoggedInToAnthropic(): boolean { 10 | return false 11 | // const config = getGlobalConfig() 12 | // return !!config.primaryApiKey 13 | } 14 | -------------------------------------------------------------------------------- /src/components/AsciiLogo.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from 'ink' 2 | import React from 'react' 3 | import { getTheme } from '../utils/theme' 4 | import { ASCII_LOGO } from '../constants/product' 5 | 6 | export function AsciiLogo(): React.ReactNode { 7 | const theme = getTheme() 8 | return ( 9 | 10 | {ASCII_LOGO} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/http.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP utility constants and helpers 3 | */ 4 | 5 | import { MACRO } from '../constants/macros' 6 | import { PRODUCT_COMMAND } from '../constants/product' 7 | 8 | // WARNING: We rely on `claude-cli` in the user agent for log filtering. 9 | // Please do NOT change this without making sure that logging also gets updated! 10 | export const USER_AGENT = `${PRODUCT_COMMAND}/${MACRO.VERSION} (${process.env.USER_TYPE})` 11 | -------------------------------------------------------------------------------- /src/components/StickerRequestForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface FormData { 4 | // Define form data structure as needed 5 | [key: string]: any 6 | } 7 | 8 | export interface StickerRequestFormProps { 9 | // Define props as needed 10 | onSubmit?: (data: FormData) => void 11 | } 12 | 13 | export const StickerRequestForm: React.FC = () => { 14 | // Minimal component implementation 15 | return null 16 | } -------------------------------------------------------------------------------- /src/utils/browser.ts: -------------------------------------------------------------------------------- 1 | import { execFileNoThrow } from './execFileNoThrow' 2 | 3 | export async function openBrowser(url: string): Promise { 4 | const platform = process.platform 5 | const command = 6 | platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open' 7 | 8 | try { 9 | const { code } = await execFileNoThrow(command, [url]) 10 | return code === 0 11 | } catch (_) { 12 | return false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/tools/NotebookReadTool/prompt.ts: -------------------------------------------------------------------------------- 1 | export const DESCRIPTION = 2 | 'Extract and read source code from all code cells in a Jupyter notebook.' 3 | export const PROMPT = `Reads a Jupyter notebook (.ipynb file) and returns all of the cells with their outputs. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path.` 4 | -------------------------------------------------------------------------------- /src/commands/cost.ts: -------------------------------------------------------------------------------- 1 | import type { Command } from '../commands' 2 | import { formatTotalCost } from '../cost-tracker' 3 | 4 | const cost = { 5 | type: 'local', 6 | name: 'cost', 7 | description: 'Show the total cost and duration of the current session', 8 | isEnabled: true, 9 | isHidden: false, 10 | async call() { 11 | return formatTotalCost() 12 | }, 13 | userFacingName() { 14 | return 'cost' 15 | }, 16 | } satisfies Command 17 | 18 | export default cost 19 | -------------------------------------------------------------------------------- /src/tools/GlobTool/prompt.ts: -------------------------------------------------------------------------------- 1 | export const TOOL_NAME_FOR_PROMPT = 'GlobTool' 2 | 3 | export const DESCRIPTION = `- Fast file pattern matching tool that works with any codebase size 4 | - Supports glob patterns like "**/*.js" or "src/**/*.ts" 5 | - Returns matching file paths sorted by modification time 6 | - Use this tool when you need to find files by name patterns 7 | - When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead 8 | ` 9 | -------------------------------------------------------------------------------- /src/tools/FileWriteTool/prompt.ts: -------------------------------------------------------------------------------- 1 | export const PROMPT = `Write a file to the local filesystem. Overwrites the existing file if there is one. 2 | 3 | Before using this tool: 4 | 5 | 1. Use the ReadFile tool to understand the file's contents and context 6 | 7 | 2. Directory Verification (only applicable when creating new files): 8 | - Use the LS tool to verify the parent directory exists and is the correct location` 9 | 10 | export const DESCRIPTION = 'Write a file to the local filesystem.' 11 | -------------------------------------------------------------------------------- /src/commands/config.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from '../commands' 2 | import { Config } from '../components/Config' 3 | import * as React from 'react' 4 | 5 | const config = { 6 | type: 'local-jsx', 7 | name: 'config', 8 | description: 'Open config panel', 9 | isEnabled: true, 10 | isHidden: false, 11 | async call(onDone) { 12 | return 13 | }, 14 | userFacingName() { 15 | return 'config' 16 | }, 17 | } satisfies Command 18 | 19 | export default config 20 | -------------------------------------------------------------------------------- /src/components/FallbackToolUseRejectedMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { getTheme } from '../utils/theme' 3 | import { Text } from 'ink' 4 | import { PRODUCT_NAME } from '../constants/product' 5 | 6 | export function FallbackToolUseRejectedMessage(): React.ReactNode { 7 | return ( 8 | 9 |   ⎿   10 | 11 | No (tell {PRODUCT_NAME} what to do differently) 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/useLogMessages.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { type Message } from '../query' 3 | import { overwriteLog, getMessagesPath } from '../utils/log' 4 | 5 | export function useLogMessages( 6 | messages: Message[], 7 | messageLogName: string, 8 | forkNumber: number, 9 | ): void { 10 | useEffect(() => { 11 | overwriteLog( 12 | getMessagesPath(messageLogName, forkNumber, 0), 13 | messages.filter(_ => _.type !== 'progress'), 14 | ) 15 | }, [messages, messageLogName, forkNumber]) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/messages/AssistantRedactedThinkingMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box, Text } from 'ink' 3 | import { getTheme } from '../../utils/theme' 4 | 5 | type Props = { 6 | addMargin: boolean 7 | } 8 | 9 | export function AssistantRedactedThinkingMessage({ 10 | addMargin = false, 11 | }: Props): React.ReactNode { 12 | return ( 13 | 14 | 15 | ✻ Thinking… 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/bug.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from '../commands' 2 | import { Bug } from '../components/Bug' 3 | import * as React from 'react' 4 | import { PRODUCT_NAME } from '../constants/product' 5 | 6 | const bug = { 7 | type: 'local-jsx', 8 | name: 'bug', 9 | description: `Submit feedback about ${PRODUCT_NAME}`, 10 | isEnabled: true, 11 | isHidden: false, 12 | async call(onDone) { 13 | return 14 | }, 15 | userFacingName() { 16 | return 'bug' 17 | }, 18 | } satisfies Command 19 | 20 | export default bug 21 | -------------------------------------------------------------------------------- /src/commands/help.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from '../commands' 2 | import { Help } from '../components/Help' 3 | import * as React from 'react' 4 | 5 | const help = { 6 | type: 'local-jsx', 7 | name: 'help', 8 | description: 'Show help and available commands', 9 | isEnabled: true, 10 | isHidden: false, 11 | async call(onDone, context) { 12 | return 13 | }, 14 | userFacingName() { 15 | return 'help' 16 | }, 17 | } satisfies Command 18 | 19 | export default help 20 | -------------------------------------------------------------------------------- /src/constants/oauth.ts: -------------------------------------------------------------------------------- 1 | const BASE_CONFIG = { 2 | REDIRECT_PORT: 54545, 3 | MANUAL_REDIRECT_URL: '/oauth/code/callback', 4 | SCOPES: ['org:create_api_key', 'user:profile'] as const, 5 | } 6 | 7 | // Production OAuth configuration - Used in normal operation 8 | const PROD_OAUTH_CONFIG = { 9 | ...BASE_CONFIG, 10 | AUTHORIZE_URL: '', 11 | TOKEN_URL: '', 12 | API_KEY_URL: '', 13 | SUCCESS_URL: '', 14 | CLIENT_ID: '', 15 | } as const 16 | 17 | // Default to prod config, override with test/staging if enabled 18 | export const OAUTH_CONFIG = PROD_OAUTH_CONFIG 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug Description 11 | 12 | 13 | 14 | ## App and Environment Info 15 | Kode Version: 16 | OS: 17 | 18 | ## Models 19 | baseURL: 20 | name: 21 | maxTokens: 22 | reasoning effort: 23 | -------------------------------------------------------------------------------- /src/components/Cost.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Box, Text } from 'ink' 3 | 4 | type Props = { 5 | costUSD: number 6 | durationMs: number 7 | debug: boolean 8 | } 9 | 10 | export function Cost({ costUSD, durationMs, debug }: Props): React.ReactNode { 11 | if (!debug) { 12 | return null 13 | } 14 | 15 | const durationInSeconds = (durationMs / 1000).toFixed(1) 16 | return ( 17 | 18 | 19 | Cost: ${costUSD.toFixed(4)} ({durationInSeconds}s) 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/modelstatus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Command } from '../commands' 3 | import { ModelStatusDisplay } from '../components/ModelStatusDisplay' 4 | 5 | const modelstatus: Command = { 6 | name: 'modelstatus', 7 | description: 'Display current model configuration and status', 8 | aliases: ['ms', 'model-status'], 9 | isEnabled: true, 10 | isHidden: false, 11 | userFacingName() { 12 | return 'modelstatus' 13 | }, 14 | type: 'local-jsx', 15 | call(onDone) { 16 | return Promise.resolve() 17 | }, 18 | } 19 | 20 | export default modelstatus 21 | -------------------------------------------------------------------------------- /src/components/permissions/utils.ts: -------------------------------------------------------------------------------- 1 | import { env } from '../../utils/env' 2 | import { CompletionType, logUnaryEvent } from '../../utils/unaryLogging' 3 | import { ToolUseConfirm } from './PermissionRequest' 4 | 5 | export function logUnaryPermissionEvent( 6 | completion_type: CompletionType, 7 | { 8 | assistantMessage: { 9 | message: { id: message_id }, 10 | }, 11 | }: ToolUseConfirm, 12 | event: 'accept' | 'reject', 13 | ): void { 14 | logUnaryEvent({ 15 | completion_type, 16 | event, 17 | metadata: { 18 | language_name: 'none', 19 | message_id, 20 | platform: env.platform, 21 | }, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/tools/NotebookEditTool/prompt.ts: -------------------------------------------------------------------------------- 1 | export const DESCRIPTION = 2 | 'Replace the contents of a specific cell in a Jupyter notebook.' 3 | export const PROMPT = `Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.` 4 | -------------------------------------------------------------------------------- /src/tools/GrepTool/prompt.ts: -------------------------------------------------------------------------------- 1 | export const TOOL_NAME_FOR_PROMPT = 'GrepTool' 2 | 3 | export const DESCRIPTION = ` 4 | - Fast content search tool that works with any codebase size 5 | - Searches file contents using regular expressions 6 | - Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.) 7 | - Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}") 8 | - Returns matching file paths sorted by modification time 9 | - Use this tool when you need to find files containing specific patterns 10 | - When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead 11 | ` 12 | -------------------------------------------------------------------------------- /src/history.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getCurrentProjectConfig, 3 | saveCurrentProjectConfig, 4 | } from './utils/config.js' 5 | 6 | const MAX_HISTORY_ITEMS = 100 7 | 8 | export function getHistory(): string[] { 9 | return getCurrentProjectConfig().history ?? [] 10 | } 11 | 12 | export function addToHistory(command: string): void { 13 | const projectConfig = getCurrentProjectConfig() 14 | const history = projectConfig.history ?? [] 15 | 16 | if (history[0] === command) { 17 | return 18 | } 19 | 20 | history.unshift(command) 21 | saveCurrentProjectConfig({ 22 | ...projectConfig, 23 | history: history.slice(0, MAX_HISTORY_ITEMS), 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/state.ts: -------------------------------------------------------------------------------- 1 | import { cwd } from 'process' 2 | import { PersistentShell } from './PersistentShell' 3 | 4 | // DO NOT ADD MORE STATE HERE OR BORIS WILL CURSE YOU 5 | const STATE: { 6 | originalCwd: string 7 | } = { 8 | originalCwd: cwd(), 9 | } 10 | 11 | export async function setCwd(cwd: string): Promise { 12 | await PersistentShell.getInstance().setCwd(cwd) 13 | } 14 | 15 | export function setOriginalCwd(cwd: string): void { 16 | STATE.originalCwd = cwd 17 | } 18 | 19 | export function getOriginalCwd(): string { 20 | return STATE.originalCwd 21 | } 22 | 23 | export function getCwd(): string { 24 | return PersistentShell.getInstance().pwd() 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/doctor.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Command } from '../commands' 3 | import { Doctor } from '../screens/Doctor' 4 | import { PRODUCT_NAME } from '../constants/product' 5 | 6 | const doctor: Command = { 7 | name: 'doctor', 8 | description: `Checks the health of your ${PRODUCT_NAME} installation`, 9 | isEnabled: true, 10 | isHidden: false, 11 | userFacingName() { 12 | return 'doctor' 13 | }, 14 | type: 'local-jsx', 15 | call(onDone) { 16 | const element = React.createElement(Doctor, { 17 | onDone, 18 | doctorMode: true, 19 | }) 20 | return Promise.resolve(element) 21 | }, 22 | } 23 | 24 | export default doctor 25 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export class MalformedCommandError extends TypeError {} 2 | 3 | export class DeprecatedCommandError extends Error {} 4 | 5 | export class AbortError extends Error {} 6 | 7 | /** 8 | * Custom error class for configuration file parsing errors 9 | * Includes the file path and the default configuration that should be used 10 | */ 11 | export class ConfigParseError extends Error { 12 | filePath: string 13 | defaultConfig: unknown 14 | 15 | constructor(message: string, filePath: string, defaultConfig: unknown) { 16 | super(message) 17 | this.name = 'ConfigParseError' 18 | this.filePath = filePath 19 | this.defaultConfig = defaultConfig 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/betas.ts: -------------------------------------------------------------------------------- 1 | import { memoize } from 'lodash-es' 2 | import { checkGate } from '../services/statsig' 3 | import { 4 | GATE_TOKEN_EFFICIENT_TOOLS, 5 | BETA_HEADER_TOKEN_EFFICIENT_TOOLS, 6 | CLAUDE_CODE_20250219_BETA_HEADER, 7 | } from '../constants/betas.js' 8 | 9 | export const getBetas = memoize(async (): Promise => { 10 | const betaHeaders = [CLAUDE_CODE_20250219_BETA_HEADER] 11 | 12 | if (process.env.USER_TYPE === 'ant' || process.env.SWE_BENCH) { 13 | const useTokenEfficientTools = await checkGate(GATE_TOKEN_EFFICIENT_TOOLS) 14 | if (useTokenEfficientTools) { 15 | betaHeaders.push(BETA_HEADER_TOKEN_EFFICIENT_TOOLS) 16 | } 17 | } 18 | 19 | return betaHeaders 20 | }) 21 | -------------------------------------------------------------------------------- /src/utils/unaryLogging.ts: -------------------------------------------------------------------------------- 1 | import { logEvent } from '../services/statsig' 2 | 3 | export type CompletionType = 4 | | 'str_replace_single' 5 | | 'write_file_single' 6 | | 'tool_use_single' 7 | 8 | type LogEvent = { 9 | completion_type: CompletionType 10 | event: 'accept' | 'reject' | 'response' 11 | metadata: { 12 | language_name: string 13 | message_id: string 14 | platform: string 15 | } 16 | } 17 | 18 | export function logUnaryEvent(event: LogEvent): void { 19 | logEvent('tengu_unary_event', { 20 | event: event.event, 21 | completion_type: event.completion_type, 22 | language_name: event.metadata.language_name, 23 | message_id: event.metadata.message_id, 24 | platform: event.metadata.platform, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/responseState.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Response state management for Responses API 3 | * Tracks previous_response_id for conversation chaining 4 | */ 5 | 6 | // Store the last response ID for each conversation 7 | const responseIdCache = new Map() 8 | 9 | export function getLastResponseId(conversationId: string): string | undefined { 10 | return responseIdCache.get(conversationId) 11 | } 12 | 13 | export function setLastResponseId(conversationId: string, responseId: string): void { 14 | responseIdCache.set(conversationId, responseId) 15 | } 16 | 17 | export function clearResponseId(conversationId: string): void { 18 | responseIdCache.delete(conversationId) 19 | } 20 | 21 | export function clearAllResponseIds(): void { 22 | responseIdCache.clear() 23 | } -------------------------------------------------------------------------------- /src/components/messages/AssistantBashOutputMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import BashToolResultMessage from '../../tools/BashTool/BashToolResultMessage' 3 | import { extractTag } from '../../utils/messages' 4 | 5 | export function AssistantBashOutputMessage({ 6 | content, 7 | verbose, 8 | }: { 9 | content: string 10 | verbose?: boolean 11 | }): React.ReactNode { 12 | const stdout = extractTag(content, 'bash-stdout') ?? '' 13 | const stderr = extractTag(content, 'bash-stderr') ?? '' 14 | const stdoutLines = stdout.split('\n').length 15 | const stderrLines = stderr.split('\n').length 16 | return ( 17 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | /** 4 | * A custom hook that runs a callback at a specified interval. 5 | * The interval is cleared when the component unmounts. 6 | * The interval is also cleared and restarted if the delay changes. 7 | */ 8 | export function useInterval(callback: () => void, delay: number): void { 9 | const savedCallback = useRef(callback) 10 | 11 | // Remember the latest callback 12 | useEffect(() => { 13 | savedCallback.current = callback 14 | }, [callback]) 15 | 16 | // Set up the interval 17 | useEffect(() => { 18 | function tick() { 19 | savedCallback.current() 20 | } 21 | 22 | const id = setInterval(tick, delay) 23 | return () => clearInterval(id) 24 | }, [delay]) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/SentryErrorBoundary.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { captureException } from '../services/sentry' 3 | 4 | interface Props { 5 | children: React.ReactNode 6 | } 7 | 8 | interface State { 9 | hasError: boolean 10 | } 11 | 12 | export class SentryErrorBoundary extends React.Component { 13 | constructor(props: Props) { 14 | super(props) 15 | ;(this as any).state = { hasError: false } 16 | } 17 | 18 | static getDerivedStateFromError(): State { 19 | return { hasError: true } 20 | } 21 | 22 | componentDidCatch(error: Error): void { 23 | captureException(error) 24 | } 25 | 26 | render(): React.ReactNode { 27 | if ((this as any).state.hasError) { 28 | return null 29 | } 30 | 31 | return (this as any).props.children 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/CustomSelect/use-select.ts: -------------------------------------------------------------------------------- 1 | import { useInput } from 'ink' 2 | import { type SelectState } from './use-select-state' 3 | 4 | export type UseSelectProps = { 5 | /** 6 | * When disabled, user input is ignored. 7 | * 8 | * @default false 9 | */ 10 | isDisabled?: boolean 11 | 12 | /** 13 | * Select state. 14 | */ 15 | state: SelectState 16 | } 17 | 18 | export const useSelect = ({ isDisabled = false, state }: UseSelectProps) => { 19 | useInput( 20 | (_input, key) => { 21 | if (key.downArrow) { 22 | state.focusNextOption() 23 | } 24 | 25 | if (key.upArrow) { 26 | state.focusPreviousOption() 27 | } 28 | 29 | if (key.return) { 30 | state.selectFocusedOption() 31 | } 32 | }, 33 | { isActive: !isDisabled }, 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/tools/FileReadTool/prompt.ts: -------------------------------------------------------------------------------- 1 | import { NotebookReadTool } from '../NotebookReadTool/NotebookReadTool' 2 | 3 | const MAX_LINES_TO_READ = 2000 4 | const MAX_LINE_LENGTH = 2000 5 | 6 | export const DESCRIPTION = 'Read a file from the local filesystem.' 7 | export const PROMPT = `Reads a file from the local filesystem. The file_path parameter must be an absolute path, not a relative path. By default, it reads up to ${MAX_LINES_TO_READ} lines starting from the beginning of the file. You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters. Any lines longer than ${MAX_LINE_LENGTH} characters will be truncated. For image files, the tool will display the image for you. For Jupyter notebooks (.ipynb files), use the ${NotebookReadTool.name} instead.` 8 | -------------------------------------------------------------------------------- /src/constants/product.ts: -------------------------------------------------------------------------------- 1 | export const PRODUCT_NAME = 'Kode' 2 | export const PRODUCT_URL = 'https://github.com/shareAI-lab/Anykode' 3 | export const PROJECT_FILE = 'AGENTS.md' 4 | export const PRODUCT_COMMAND = 'kode' 5 | export const CONFIG_BASE_DIR = '.kode' 6 | export const CONFIG_FILE = '.kode.json' 7 | export const GITHUB_ISSUES_REPO_URL = 8 | 'https://github.com/shareAI-lab/Anykode/issues' 9 | 10 | export const ASCII_LOGO = ` 11 | _ _ _ __ _ 12 | | | __ _ ___ | |_ | |/ / ___ __| | ___ 13 | | | / _\` | / __| | __| | ' / / _ \\ / _\` | / _ \\ 14 | | |___ | (_| | \\__ \\ | |_ | . \\ | (_) | | (_| | | __/ 15 | |_____| \\__,_| |___/ \\__| |_|\\_\\ \\___/ \\__,_| \\___| 16 | 17 | ` 18 | -------------------------------------------------------------------------------- /src/components/messages/UserBashInputMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from 'ink' 2 | import * as React from 'react' 3 | import { extractTag } from '../../utils/messages' 4 | import { getTheme } from '../../utils/theme' 5 | import { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' 6 | 7 | type Props = { 8 | addMargin: boolean 9 | param: TextBlockParam 10 | } 11 | 12 | export function UserBashInputMessage({ 13 | param: { text }, 14 | addMargin, 15 | }: Props): React.ReactNode { 16 | const input = extractTag(text, 'bash-input') 17 | if (!input) { 18 | return null 19 | } 20 | return ( 21 | 22 | 23 | ! 24 | {input} 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/messages/UserKodingInputMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from 'ink' 2 | import * as React from 'react' 3 | import { extractTag } from '../../utils/messages' 4 | import { getTheme } from '../../utils/theme' 5 | import { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' 6 | 7 | type Props = { 8 | addMargin: boolean 9 | param: TextBlockParam 10 | } 11 | 12 | export function UserKodingInputMessage({ 13 | param: { text }, 14 | addMargin, 15 | }: Props): React.ReactNode { 16 | const input = extractTag(text, 'koding-input') 17 | if (!input) { 18 | return null 19 | } 20 | return ( 21 | 22 | 23 | # 24 | {input} 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useExitOnCtrlCD.ts: -------------------------------------------------------------------------------- 1 | import { useInput } from 'ink' 2 | import { useDoublePress } from './useDoublePress' 3 | import { useState } from 'react' 4 | 5 | type ExitState = { 6 | pending: boolean 7 | keyName: 'Ctrl-C' | 'Ctrl-D' | null 8 | } 9 | 10 | export function useExitOnCtrlCD(onExit: () => void): ExitState { 11 | const [exitState, setExitState] = useState({ 12 | pending: false, 13 | keyName: null, 14 | }) 15 | 16 | const handleCtrlC = useDoublePress( 17 | pending => setExitState({ pending, keyName: 'Ctrl-C' }), 18 | onExit, 19 | ) 20 | const handleCtrlD = useDoublePress( 21 | pending => setExitState({ pending, keyName: 'Ctrl-D' }), 22 | onExit, 23 | ) 24 | 25 | useInput((input, key) => { 26 | if (key.ctrl && input === 'c') handleCtrlC() 27 | if (key.ctrl && input === 'd') handleCtrlD() 28 | }) 29 | 30 | return exitState 31 | } 32 | -------------------------------------------------------------------------------- /src/components/TokenWarning.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from 'ink' 2 | import * as React from 'react' 3 | import { getTheme } from '../utils/theme' 4 | 5 | type Props = { 6 | tokenUsage: number 7 | } 8 | 9 | const MAX_TOKENS = 190_000 10 | export const WARNING_THRESHOLD = MAX_TOKENS * 0.6 11 | const ERROR_THRESHOLD = MAX_TOKENS * 0.8 12 | 13 | export function TokenWarning({ tokenUsage }: Props): React.ReactNode { 14 | const theme = getTheme() 15 | 16 | if (tokenUsage < WARNING_THRESHOLD) { 17 | return null 18 | } 19 | 20 | const isError = tokenUsage >= ERROR_THRESHOLD 21 | 22 | return ( 23 | 24 | 25 | Context low ( 26 | {Math.max(0, 100 - Math.round((tokenUsage / MAX_TOKENS) * 100))}% 27 | remaining) · Run /compact to compact & continue 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/messages/UserCommandMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from 'ink' 2 | import * as React from 'react' 3 | import { getTheme } from '../../utils/theme' 4 | import { extractTag } from '../../utils/messages' 5 | import { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' 6 | 7 | type Props = { 8 | addMargin: boolean 9 | param: TextBlockParam 10 | } 11 | 12 | export function UserCommandMessage({ 13 | addMargin, 14 | param: { text }, 15 | }: Props): React.ReactNode { 16 | const commandMessage = extractTag(text, 'command-message') 17 | const args = extractTag(text, 'command-args') 18 | if (!commandMessage) { 19 | return null 20 | } 21 | 22 | const theme = getTheme() 23 | return ( 24 | 25 | 26 | > /{commandMessage} {args} 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/MCPServerDialogCopy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Text } from 'ink' 3 | import Link from 'ink-link' 4 | import { PRODUCT_NAME, PRODUCT_COMMAND } from '../constants/product' 5 | 6 | export function MCPServerDialogCopy(): React.ReactNode { 7 | return ( 8 | <> 9 | 10 | MCP servers provide additional functionality to {PRODUCT_NAME}. They may 11 | execute code, make network requests, or access system resources via tool 12 | calls. All tool calls will require your explicit approval before 13 | execution. For more information, see{' '} 14 | 15 | MCP documentation 16 | 17 | 18 | 19 | 20 | Remember: You can always change these choices later by running ` 21 | {PRODUCT_COMMAND} mcp reset-mcprc-choices` 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/messages/TaskProgressMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box, Text } from 'ink' 3 | import { getTheme } from '../../utils/theme' 4 | 5 | interface Props { 6 | agentType: string 7 | status: string 8 | toolCount?: number 9 | } 10 | 11 | export function TaskProgressMessage({ agentType, status, toolCount }: Props) { 12 | const theme = getTheme() 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | [{agentType}] 20 | 21 | {status} 22 | 23 | {toolCount && toolCount > 0 && ( 24 | 25 | 26 | Tools used: {toolCount} 27 | 28 | 29 | )} 30 | 31 | ) 32 | } -------------------------------------------------------------------------------- /.kode/agents/search-specialist.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: search-specialist 3 | description: Specialized in finding files and code patterns quickly using targeted searches 4 | tools: ["Grep", "Glob", "Read", "LS"] 5 | color: green 6 | --- 7 | 8 | You are a search specialist optimized for quickly finding files, code patterns, and information in codebases. 9 | 10 | Your expertise: 11 | 1. Efficient pattern matching and search strategies 12 | 2. Finding code references and dependencies 13 | 3. Locating configuration files and documentation 14 | 4. Tracing function calls and data flow 15 | 5. Discovering hidden or hard-to-find code 16 | 17 | Search strategies: 18 | - Start with broad searches and narrow down 19 | - Use multiple search patterns if the first doesn't work 20 | - Consider different naming conventions and variations 21 | - Check common locations for specific file types 22 | - Use context clues to refine searches 23 | 24 | Always aim to find all relevant occurrences, not just the first match. -------------------------------------------------------------------------------- /src/commands/onboarding.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import type { Command } from '../commands' 3 | import { Onboarding } from '../components/Onboarding' 4 | import { clearTerminal } from '../utils/terminal' 5 | import { getGlobalConfig, saveGlobalConfig } from '../utils/config' 6 | import { clearConversation } from './clear' 7 | 8 | export default { 9 | type: 'local-jsx', 10 | name: 'onboarding', 11 | description: 'Run through the onboarding flow', 12 | isEnabled: true, 13 | isHidden: false, 14 | async call(onDone, context) { 15 | await clearTerminal() 16 | const config = getGlobalConfig() 17 | saveGlobalConfig({ 18 | ...config, 19 | theme: 'dark', 20 | }) 21 | 22 | return ( 23 | { 25 | clearConversation(context) 26 | onDone() 27 | }} 28 | /> 29 | ) 30 | }, 31 | userFacingName() { 32 | return 'onboarding' 33 | }, 34 | } satisfies Command 35 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build-and-publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '18' 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Setup Bun 22 | uses: oven-sh/setup-bun@v1 23 | with: 24 | bun-version: latest 25 | 26 | - name: Setup pnpm 27 | uses: pnpm/action-setup@v2 28 | with: 29 | version: latest 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Build 35 | run: bun run build 36 | 37 | - name: Publish to npm 38 | run: pnpm publish --no-git-checks 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /src/commands/resume.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import type { Command } from '../commands' 3 | import { ResumeConversation } from '../screens/ResumeConversation' 4 | import { render } from 'ink' 5 | import { CACHE_PATHS, loadLogList } from '../utils/log' 6 | 7 | export default { 8 | type: 'local-jsx', 9 | name: 'resume', 10 | description: 'Resume a previous conversation', 11 | isEnabled: true, 12 | isHidden: false, 13 | userFacingName() { 14 | return 'resume' 15 | }, 16 | async call(onDone, context) { 17 | const { commands = [], tools = [], verbose = false } = context.options || {} 18 | const logs = await loadLogList(CACHE_PATHS.messages()) 19 | render( 20 | , 27 | ) 28 | // This return is here for type only 29 | return null 30 | }, 31 | } satisfies Command 32 | -------------------------------------------------------------------------------- /src/utils/style.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'fs' 2 | import { join, parse, dirname } from 'path' 3 | import { memoize } from 'lodash-es' 4 | import { getCwd } from './state' 5 | import { PROJECT_FILE } from '../constants/product' 6 | 7 | const STYLE_PROMPT = 8 | 'The codebase follows strict style guidelines shown below. All code changes must strictly adhere to these guidelines to maintain consistency and quality.' 9 | 10 | export const getCodeStyle = memoize((): string => { 11 | const styles: string[] = [] 12 | let currentDir = getCwd() 13 | 14 | while (currentDir !== parse(currentDir).root) { 15 | const stylePath = join(currentDir, PROJECT_FILE) 16 | if (existsSync(stylePath)) { 17 | styles.push( 18 | `Contents of ${stylePath}:\n\n${readFileSync(stylePath, 'utf-8')}`, 19 | ) 20 | } 21 | currentDir = dirname(currentDir) 22 | } 23 | 24 | if (styles.length === 0) { 25 | return '' 26 | } 27 | 28 | return `${STYLE_PROMPT}\n\n${styles.reverse().join('\n\n')}` 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Tool } from '../../../Tool' 3 | import { Message } from '../../../query' 4 | import { FallbackToolUseRejectedMessage } from '../../FallbackToolUseRejectedMessage' 5 | import { useGetToolFromMessages } from './utils' 6 | import { useTerminalSize } from '../../../hooks/useTerminalSize' 7 | 8 | type Props = { 9 | toolUseID: string 10 | messages: Message[] 11 | tools: Tool[] 12 | verbose: boolean 13 | } 14 | 15 | export function UserToolRejectMessage({ 16 | toolUseID, 17 | tools, 18 | messages, 19 | verbose, 20 | }: Props): React.ReactNode { 21 | const { columns } = useTerminalSize() 22 | const { tool, toolUse } = useGetToolFromMessages(toolUseID, tools, messages) 23 | const input = tool.inputSchema.safeParse(toolUse.input) 24 | if (input.success) { 25 | return tool.renderToolUseRejectedMessage(input.data, { 26 | columns, 27 | verbose, 28 | }) 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /src/components/messages/AssistantThinkingMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box, Text } from 'ink' 3 | import { getTheme } from '../../utils/theme' 4 | import { applyMarkdown } from '../../utils/markdown' 5 | import { 6 | ThinkingBlock, 7 | ThinkingBlockParam, 8 | } from '@anthropic-ai/sdk/resources/index.mjs' 9 | 10 | type Props = { 11 | param: ThinkingBlock | ThinkingBlockParam 12 | addMargin: boolean 13 | } 14 | 15 | export function AssistantThinkingMessage({ 16 | param: { thinking }, 17 | addMargin = false, 18 | }: Props): React.ReactNode { 19 | if (!thinking) { 20 | return null 21 | } 22 | 23 | return ( 24 | 30 | 31 | ✻ Thinking… 32 | 33 | 34 | 35 | {applyMarkdown(thinking)} 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/messages.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Message } from './query' 3 | 4 | let getMessages: () => Message[] = () => [] 5 | let setMessages: React.Dispatch> = () => {} 6 | 7 | export function setMessagesGetter(getter: () => Message[]) { 8 | getMessages = getter 9 | } 10 | 11 | export function getMessagesGetter(): () => Message[] { 12 | return getMessages 13 | } 14 | 15 | export function setMessagesSetter( 16 | setter: React.Dispatch>, 17 | ) { 18 | setMessages = setter 19 | } 20 | 21 | export function getMessagesSetter(): React.Dispatch< 22 | React.SetStateAction 23 | > { 24 | return setMessages 25 | } 26 | 27 | // Global UI refresh mechanism for model configuration changes 28 | let onModelConfigChange: (() => void) | null = null 29 | 30 | export function setModelConfigChangeHandler(handler: () => void) { 31 | onModelConfigChange = handler 32 | } 33 | 34 | export function triggerModelConfigChange() { 35 | if (onModelConfigChange) { 36 | onModelConfigChange() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx: -------------------------------------------------------------------------------- 1 | import { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' 2 | import { Box } from 'ink' 3 | import * as React from 'react' 4 | import { Tool } from '../../../Tool' 5 | import { Message, UserMessage } from '../../../query' 6 | import { useGetToolFromMessages } from './utils' 7 | 8 | type Props = { 9 | param: ToolResultBlockParam 10 | message: UserMessage 11 | messages: Message[] 12 | verbose: boolean 13 | tools: Tool[] 14 | width: number | string 15 | } 16 | 17 | export function UserToolSuccessMessage({ 18 | param, 19 | message, 20 | messages, 21 | tools, 22 | verbose, 23 | width, 24 | }: Props): React.ReactNode { 25 | const { tool } = useGetToolFromMessages(param.tool_use_id, tools, messages) 26 | 27 | return ( 28 | // TODO: Distinguish UserMessage from UserToolResultMessage 29 | 30 | {tool.renderToolResultMessage?.(message.toolUseResult!.data as never, { 31 | verbose, 32 | })} 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/logout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import type { Command } from '../commands' 3 | import { getGlobalConfig, saveGlobalConfig } from '../utils/config' 4 | import { clearTerminal } from '../utils/terminal' 5 | import { Text } from 'ink' 6 | 7 | export default { 8 | type: 'local-jsx', 9 | name: 'logout', 10 | description: 'Sign out from your Anthropic account', 11 | isEnabled: true, 12 | isHidden: false, 13 | async call() { 14 | await clearTerminal() 15 | 16 | const config = getGlobalConfig() 17 | 18 | config.oauthAccount = undefined 19 | config.hasCompletedOnboarding = false 20 | 21 | if (config.customApiKeyResponses?.approved) { 22 | config.customApiKeyResponses.approved = [] 23 | } 24 | 25 | saveGlobalConfig(config) 26 | 27 | const message = ( 28 | Successfully logged out from your Anthropic account. 29 | ) 30 | 31 | setTimeout(() => { 32 | process.exit(0) 33 | }, 200) 34 | 35 | return message 36 | }, 37 | userFacingName() { 38 | return 'logout' 39 | }, 40 | } satisfies Command 41 | -------------------------------------------------------------------------------- /src/services/notifier.ts: -------------------------------------------------------------------------------- 1 | import { getGlobalConfig } from '../utils/config' 2 | 3 | export type NotificationOptions = { 4 | message: string 5 | title?: string 6 | } 7 | 8 | function sendITerm2Notification({ message, title }: NotificationOptions): void { 9 | const displayString = title ? `${title}:\n${message}` : message 10 | try { 11 | process.stdout.write(`\x1b]9;\n\n${displayString}\x07`) 12 | } catch { 13 | // Ignore errors 14 | } 15 | } 16 | 17 | function sendTerminalBell(): void { 18 | process.stdout.write('\x07') 19 | } 20 | 21 | export async function sendNotification( 22 | notif: NotificationOptions, 23 | ): Promise { 24 | const channel = getGlobalConfig().preferredNotifChannel 25 | switch (channel) { 26 | case 'iterm2': 27 | sendITerm2Notification(notif) 28 | break 29 | case 'terminal_bell': 30 | sendTerminalBell() 31 | break 32 | case 'iterm2_with_bell': 33 | sendITerm2Notification(notif) 34 | sendTerminalBell() 35 | break 36 | case 'notifications_disabled': 37 | // Do nothing 38 | break 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/messages/UserPromptMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' 3 | import { Box, Text } from 'ink' 4 | import { getTheme } from '../../utils/theme' 5 | import { logError } from '../../utils/log' 6 | import { useTerminalSize } from '../../hooks/useTerminalSize' 7 | 8 | type Props = { 9 | addMargin: boolean 10 | param: TextBlockParam 11 | } 12 | 13 | export function UserPromptMessage({ 14 | addMargin, 15 | param: { text }, 16 | }: Props): React.ReactNode { 17 | const { columns } = useTerminalSize() 18 | if (!text) { 19 | logError('No content found in user prompt message') 20 | return null 21 | } 22 | 23 | return ( 24 | 25 | 26 | > 27 | 28 | 29 | 30 | {text} 31 | 32 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/tools/ThinkTool/prompt.ts: -------------------------------------------------------------------------------- 1 | export const DESCRIPTION = 2 | 'This is a no-op tool that logs a thought. It is inspired by the tau-bench think tool.' 3 | export const PROMPT = `Use the tool to think about something. It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed. 4 | 5 | Common use cases: 6 | 1. When exploring a repository and discovering the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective 7 | 2. After receiving test results, use this tool to brainstorm ways to fix failing tests 8 | 3. When planning a complex refactoring, use this tool to outline different approaches and their tradeoffs 9 | 4. When designing a new feature, use this tool to think through architecture decisions and implementation details 10 | 5. When debugging a complex issue, use this tool to organize your thoughts and hypotheses 11 | 12 | The tool simply logs your thought process for better transparency and does not execute any code or make changes.` 13 | -------------------------------------------------------------------------------- /src/components/ToolUseLoader.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from 'ink' 2 | import React from 'react' 3 | import { useInterval } from '../hooks/useInterval' 4 | import { getTheme } from '../utils/theme' 5 | import { BLACK_CIRCLE } from '../constants/figures' 6 | 7 | type Props = { 8 | isError: boolean 9 | isUnresolved: boolean 10 | shouldAnimate: boolean 11 | } 12 | 13 | export function ToolUseLoader({ 14 | isError, 15 | isUnresolved, 16 | shouldAnimate, 17 | }: Props): React.ReactNode { 18 | const [isVisible, setIsVisible] = React.useState(true) 19 | 20 | useInterval(() => { 21 | if (!shouldAnimate) { 22 | return 23 | } 24 | // To avoid flickering when the tool use confirm is visible, we set the loader to be visible 25 | // when the tool use confirm is visible. 26 | setIsVisible(_ => !_) 27 | }, 600) 28 | 29 | const color = isUnresolved 30 | ? getTheme().secondaryText 31 | : isError 32 | ? getTheme().error 33 | : getTheme().success 34 | 35 | return ( 36 | 37 | {isVisible ? BLACK_CIRCLE : ' '} 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/HighlightedCode.tsx: -------------------------------------------------------------------------------- 1 | import { highlight, supportsLanguage } from 'cli-highlight' 2 | import { Text } from 'ink' 3 | import React, { useMemo } from 'react' 4 | import { logError } from '../utils/log' 5 | 6 | type Props = { 7 | code: string 8 | language: string 9 | } 10 | 11 | export function HighlightedCode({ code, language }: Props): React.ReactElement { 12 | const highlightedCode = useMemo(() => { 13 | try { 14 | if (supportsLanguage(language)) { 15 | return highlight(code, { language }) 16 | } else { 17 | logError( 18 | `Language not supported while highlighting code, falling back to markdown: ${language}`, 19 | ) 20 | return highlight(code, { language: 'markdown' }) 21 | } 22 | } catch (e) { 23 | if (e instanceof Error && e.message.includes('Unknown language')) { 24 | logError( 25 | `Language not supported while highlighting code, falling back to markdown: ${e}`, 26 | ) 27 | return highlight(code, { language: 'markdown' }) 28 | } 29 | } 30 | }, [code, language]) 31 | 32 | return {highlightedCode} 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/release-notes.ts: -------------------------------------------------------------------------------- 1 | import { MACRO } from '../constants/macros.js' 2 | import type { Command } from '../commands' 3 | import { RELEASE_NOTES } from '../constants/releaseNotes' 4 | 5 | const releaseNotes: Command = { 6 | description: 'Show release notes for the current or specified version', 7 | isEnabled: false, 8 | isHidden: false, 9 | name: 'release-notes', 10 | userFacingName() { 11 | return 'release-notes' 12 | }, 13 | type: 'local', 14 | async call(args) { 15 | const currentVersion = MACRO.VERSION 16 | 17 | // If a specific version is requested, show that version's notes 18 | const requestedVersion = args ? args.trim() : currentVersion 19 | 20 | // Get the requested version's notes 21 | const notes = RELEASE_NOTES[requestedVersion] 22 | 23 | if (!notes || notes.length === 0) { 24 | return `No release notes available for version ${requestedVersion}.` 25 | } 26 | 27 | const header = `Release notes for version ${requestedVersion}:` 28 | const formattedNotes = notes.map(note => `• ${note}`).join('\n') 29 | 30 | return `${header}\n\n${formattedNotes}` 31 | }, 32 | } 33 | 34 | export default releaseNotes 35 | -------------------------------------------------------------------------------- /.kode/agents/code-writer.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: code-writer 3 | description: Specialized in writing and modifying code, implementing features, fixing bugs, and refactoring 4 | tools: ["Read", "Write", "Edit", "MultiEdit", "Bash"] 5 | color: blue 6 | --- 7 | 8 | You are a code writing specialist focused on implementing features, fixing bugs, and refactoring code. 9 | 10 | Your primary responsibilities: 11 | 1. Write clean, maintainable, and well-tested code 12 | 2. Follow existing project conventions and patterns 13 | 3. Implement features according to specifications 14 | 4. Fix bugs with minimal side effects 15 | 5. Refactor code to improve quality and maintainability 16 | 17 | Guidelines: 18 | - Always understand the existing code structure before making changes 19 | - Write code that fits naturally with the surrounding codebase 20 | - Consider edge cases and error handling 21 | - Keep changes focused and avoid scope creep 22 | - Test your changes when possible 23 | 24 | When implementing features: 25 | - Start by understanding the requirements fully 26 | - Review existing similar code for patterns to follow 27 | - Implement incrementally with clear commits 28 | - Ensure backward compatibility where needed -------------------------------------------------------------------------------- /src/components/CustomSelect/theme.ts: -------------------------------------------------------------------------------- 1 | // Theme type definitions for CustomSelect components 2 | // Used by select.tsx and select-option.tsx 3 | 4 | import type { BoxProps, TextProps } from 'ink' 5 | 6 | /** 7 | * Theme interface for CustomSelect components 8 | * Defines the style functions used by the select components 9 | */ 10 | export interface Theme { 11 | /** 12 | * Collection of style functions 13 | */ 14 | styles: { 15 | /** 16 | * Container styles for the select box 17 | */ 18 | container(): BoxProps 19 | 20 | /** 21 | * Styles for individual option containers 22 | */ 23 | option(props: { isFocused: boolean }): BoxProps 24 | 25 | /** 26 | * Styles for the focus indicator (arrow/pointer) 27 | */ 28 | focusIndicator(): TextProps 29 | 30 | /** 31 | * Styles for option labels 32 | */ 33 | label(props: { isFocused: boolean; isSelected: boolean }): TextProps 34 | 35 | /** 36 | * Styles for the selected indicator (checkmark) 37 | */ 38 | selectedIndicator(): TextProps 39 | 40 | /** 41 | * Styles for highlighted text in option labels 42 | */ 43 | highlightedText(): TextProps 44 | } 45 | } -------------------------------------------------------------------------------- /src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import InkLink from 'ink-link' 2 | import { Text } from 'ink' 3 | import React from 'react' 4 | import { env } from '../utils/env' 5 | 6 | type LinkProps = { 7 | url: string 8 | children?: React.ReactNode 9 | } 10 | 11 | // Terminals that support hyperlinks 12 | const LINK_SUPPORTING_TERMINALS = ['iTerm.app', 'WezTerm', 'Hyper', 'VSCode'] 13 | 14 | export default function Link({ url, children }: LinkProps): React.ReactNode { 15 | const supportsLinks = LINK_SUPPORTING_TERMINALS.includes(env.terminal ?? '') 16 | 17 | // Determine what text to display - use children or fall back to the URL itself 18 | const displayContent = children || url 19 | 20 | // Use InkLink to get clickable links when we can, or to get a nice fallback when we can't 21 | if (supportsLinks || displayContent !== url) { 22 | return ( 23 | 24 | {displayContent} 25 | 26 | ) 27 | } else { 28 | // But if we don't have a title and just have a url *and* are not a terminal that supports links 29 | // that doesn't support clickable links anyway, just show the URL 30 | return {displayContent} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/hooks/useCancelRequest.ts: -------------------------------------------------------------------------------- 1 | import { useInput } from 'ink' 2 | import { ToolUseConfirm } from '../components/permissions/PermissionRequest' 3 | import { logEvent } from '../services/statsig' 4 | import { BinaryFeedbackContext } from '../screens/REPL' 5 | import type { SetToolJSXFn } from '../Tool' 6 | 7 | export function useCancelRequest( 8 | setToolJSX: SetToolJSXFn, 9 | setToolUseConfirm: (toolUseConfirm: ToolUseConfirm | null) => void, 10 | setBinaryFeedbackContext: (bfContext: BinaryFeedbackContext | null) => void, 11 | onCancel: () => void, 12 | isLoading: boolean, 13 | isMessageSelectorVisible: boolean, 14 | abortSignal?: AbortSignal, 15 | ) { 16 | useInput((_, key) => { 17 | if (!key.escape) { 18 | return 19 | } 20 | if (abortSignal?.aborted) { 21 | return 22 | } 23 | if (!abortSignal) { 24 | return 25 | } 26 | if (!isLoading) { 27 | return 28 | } 29 | if (isMessageSelectorVisible) { 30 | // Esc closes the message selector 31 | return 32 | } 33 | logEvent('tengu_cancel', {}) 34 | setToolJSX(null) 35 | setToolUseConfirm(null) 36 | setBinaryFeedbackContext(null) 37 | onCancel() 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/CustomSelect/option-map.ts: -------------------------------------------------------------------------------- 1 | import { type Option } from '@inkjs/ui' 2 | import { optionHeaderKey, type OptionHeader } from './select' 3 | 4 | type OptionMapItem = (Option | OptionHeader) & { 5 | previous: OptionMapItem | undefined 6 | next: OptionMapItem | undefined 7 | index: number 8 | } 9 | 10 | export default class OptionMap extends Map { 11 | readonly first: OptionMapItem | undefined 12 | 13 | constructor(options: (Option | OptionHeader)[]) { 14 | const items: Array<[string, OptionMapItem]> = [] 15 | let firstItem: OptionMapItem | undefined 16 | let previous: OptionMapItem | undefined 17 | let index = 0 18 | 19 | for (const option of options) { 20 | const item = { 21 | ...option, 22 | previous, 23 | next: undefined, 24 | index, 25 | } 26 | 27 | if (previous) { 28 | previous.next = item 29 | } 30 | 31 | firstItem ||= item 32 | 33 | const key = 'value' in option ? option.value : optionHeaderKey(option) 34 | items.push([key, item]) 35 | index++ 36 | previous = item 37 | } 38 | 39 | super(items) 40 | this.first = firstItem 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/tools/BashTool/BashToolResultMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from 'ink' 2 | import { OutputLine } from './OutputLine' 3 | import React from 'react' 4 | import { getTheme } from '../../utils/theme' 5 | import { Out as BashOut } from './BashTool' 6 | 7 | type Props = { 8 | content: Omit 9 | verbose: boolean 10 | } 11 | 12 | function BashToolResultMessage({ content, verbose }: Props): React.JSX.Element { 13 | const { stdout, stdoutLines, stderr, stderrLines } = content 14 | 15 | return ( 16 | 17 | {stdout !== '' ? ( 18 | 19 | ) : null} 20 | {stderr !== '' ? ( 21 | 27 | ) : null} 28 | {stdout === '' && stderr === '' ? ( 29 | 30 |   ⎿   31 | (No content) 32 | 33 | ) : null} 34 | 35 | ) 36 | } 37 | 38 | export default BashToolResultMessage 39 | -------------------------------------------------------------------------------- /src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' 2 | import { Box, Text } from 'ink' 3 | import * as React from 'react' 4 | import { getTheme } from '../../../utils/theme' 5 | 6 | const MAX_RENDERED_LINES = 10 7 | 8 | type Props = { 9 | param: ToolResultBlockParam 10 | verbose: boolean 11 | } 12 | 13 | export function UserToolErrorMessage({ 14 | param, 15 | verbose, 16 | }: Props): React.ReactNode { 17 | const error = 18 | typeof param.content === 'string' ? param.content.trim() : 'Error' 19 | return ( 20 | 21 |   ⎿   22 | 23 | 24 | {verbose 25 | ? error 26 | : error.split('\n').slice(0, MAX_RENDERED_LINES).join('\n') || ''} 27 | 28 | {!verbose && error.split('\n').length > MAX_RENDERED_LINES && ( 29 | 30 | ... (+{error.split('\n').length - MAX_RENDERED_LINES} lines) 31 | 32 | )} 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/tools/ArchitectTool/prompt.ts: -------------------------------------------------------------------------------- 1 | export const ARCHITECT_SYSTEM_PROMPT = `You are an expert software architect. Your role is to analyze technical requirements and produce clear, actionable implementation plans. 2 | These plans will then be carried out by a junior software engineer so you need to be specific and detailed. However do not actually write the code, just explain the plan. 3 | 4 | Follow these steps for each request: 5 | 1. Carefully analyze requirements to identify core functionality and constraints 6 | 2. Define clear technical approach with specific technologies and patterns 7 | 3. Break down implementation into concrete, actionable steps at the appropriate level of abstraction 8 | 9 | Keep responses focused, specific and actionable. 10 | 11 | IMPORTANT: Do not ask the user if you should implement the changes at the end. Just provide the plan as described above. 12 | IMPORTANT: Do not attempt to write the code or use any string modification tools. Just provide the plan.` 13 | 14 | export const DESCRIPTION = 15 | 'Your go-to tool for any technical or coding task. Analyzes requirements and breaks them down into clear, actionable implementation steps. Use this whenever you need help planning how to implement a feature, solve a technical problem, or structure your code.' 16 | -------------------------------------------------------------------------------- /src/utils/diff.ts: -------------------------------------------------------------------------------- 1 | import { type Hunk, structuredPatch } from 'diff' 2 | 3 | const CONTEXT_LINES = 3 4 | 5 | // For some reason, & confuses the diff library, so we replace it with a token, 6 | // then substitute it back in after the diff is computed. 7 | const AMPERSAND_TOKEN = '<<:AMPERSAND_TOKEN:>>' 8 | 9 | const DOLLAR_TOKEN = '<<:DOLLAR_TOKEN:>>' 10 | 11 | export function getPatch({ 12 | filePath, 13 | fileContents, 14 | oldStr, 15 | newStr, 16 | }: { 17 | filePath: string 18 | fileContents: string 19 | oldStr: string 20 | newStr: string 21 | }): Hunk[] { 22 | return structuredPatch( 23 | filePath, 24 | filePath, 25 | fileContents.replaceAll('&', AMPERSAND_TOKEN).replaceAll('$', DOLLAR_TOKEN), 26 | fileContents 27 | .replaceAll('&', AMPERSAND_TOKEN) 28 | .replaceAll('$', DOLLAR_TOKEN) 29 | .replace( 30 | oldStr.replaceAll('&', AMPERSAND_TOKEN).replaceAll('$', DOLLAR_TOKEN), 31 | newStr.replaceAll('&', AMPERSAND_TOKEN).replaceAll('$', DOLLAR_TOKEN), 32 | ), 33 | undefined, 34 | undefined, 35 | { context: CONTEXT_LINES }, 36 | ).hunks.map(_ => ({ 37 | ..._, 38 | lines: _.lines.map(_ => 39 | _.replaceAll(AMPERSAND_TOKEN, '&').replaceAll(DOLLAR_TOKEN, '$'), 40 | ), 41 | })) 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/format.tsx: -------------------------------------------------------------------------------- 1 | export function wrapText(text: string, width: number): string[] { 2 | const lines: string[] = [] 3 | let currentLine = '' 4 | 5 | for (const char of text) { 6 | // Important: we need the spread to properly count multi-plane UTF-8 characters (eg. 𑚖) 7 | if ([...currentLine].length < width) { 8 | currentLine += char 9 | } else { 10 | lines.push(currentLine) 11 | currentLine = char 12 | } 13 | } 14 | 15 | if (currentLine) lines.push(currentLine) 16 | return lines 17 | } 18 | 19 | export function formatDuration(ms: number): string { 20 | if (ms < 60000) { 21 | return `${(ms / 1000).toFixed(1)}s` 22 | } 23 | 24 | const hours = Math.floor(ms / 3600000) 25 | const minutes = Math.floor((ms % 3600000) / 60000) 26 | const seconds = ((ms % 60000) / 1000).toFixed(1) 27 | 28 | if (hours > 0) { 29 | return `${hours}h ${minutes}m ${seconds}s` 30 | } 31 | if (minutes > 0) { 32 | return `${minutes}m ${seconds}s` 33 | } 34 | return `${seconds}s` 35 | } 36 | 37 | export function formatNumber(number: number): string { 38 | return new Intl.NumberFormat('en', { 39 | notation: 'compact', 40 | maximumFractionDigits: 1, 41 | }) 42 | .format(number) // eg. "1321" => "1.3K" 43 | .toLowerCase() // eg. "1.3K" => "1.3k" 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/imagePaste.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | import { readFileSync } from 'fs' 3 | 4 | const SCREENSHOT_PATH = '/tmp/claude_cli_latest_screenshot.png' 5 | 6 | export const CLIPBOARD_ERROR_MESSAGE = 7 | 'No image found in clipboard. Use Cmd + Ctrl + Shift + 4 to copy a screenshot to clipboard.' 8 | 9 | export function getImageFromClipboard(): string | null { 10 | if (process.platform !== 'darwin') { 11 | // only support image paste on macOS for now 12 | return null 13 | } 14 | 15 | try { 16 | // Check if clipboard has image 17 | execSync(`osascript -e 'the clipboard as «class PNGf»'`, { 18 | stdio: 'ignore', 19 | }) 20 | 21 | // Save the image 22 | execSync( 23 | `osascript -e 'set png_data to (the clipboard as «class PNGf»)' -e 'set fp to open for access POSIX file "${SCREENSHOT_PATH}" with write permission' -e 'write png_data to fp' -e 'close access fp'`, 24 | { stdio: 'ignore' }, 25 | ) 26 | 27 | // Read the image and convert to base64 28 | const imageBuffer = readFileSync(SCREENSHOT_PATH) 29 | const base64Image = imageBuffer.toString('base64') 30 | 31 | // Cleanup 32 | execSync(`rm -f "${SCREENSHOT_PATH}"`, { stdio: 'ignore' }) 33 | 34 | return base64Image 35 | } catch { 36 | return null 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../query' 2 | import { SYNTHETIC_ASSISTANT_MESSAGES } from './messages' 3 | 4 | export function countTokens(messages: Message[]): number { 5 | let i = messages.length - 1 6 | while (i >= 0) { 7 | const message = messages[i] 8 | if ( 9 | message?.type === 'assistant' && 10 | 'usage' in message.message && 11 | !( 12 | message.message.content[0]?.type === 'text' && 13 | SYNTHETIC_ASSISTANT_MESSAGES.has(message.message.content[0].text) 14 | ) 15 | ) { 16 | const { usage } = message.message 17 | return ( 18 | usage.input_tokens + 19 | (usage.cache_creation_input_tokens ?? 0) + 20 | (usage.cache_read_input_tokens ?? 0) + 21 | usage.output_tokens 22 | ) 23 | } 24 | i-- 25 | } 26 | return 0 27 | } 28 | 29 | export function countCachedTokens(messages: Message[]): number { 30 | let i = messages.length - 1 31 | while (i >= 0) { 32 | const message = messages[i] 33 | if (message?.type === 'assistant' && 'usage' in message.message) { 34 | const { usage } = message.message 35 | return ( 36 | (usage.cache_creation_input_tokens ?? 0) + 37 | (usage.cache_read_input_tokens ?? 0) 38 | ) 39 | } 40 | i-- 41 | } 42 | return 0 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/listen.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../commands' 2 | import { logError } from '../utils/log' 3 | import { execFileNoThrow } from '../utils/execFileNoThrow' 4 | 5 | const isEnabled = 6 | process.platform === 'darwin' && 7 | ['iTerm.app', 'Apple_Terminal'].includes(process.env.TERM_PROGRAM || '') 8 | 9 | const listen: Command = { 10 | type: 'local', 11 | name: 'listen', 12 | description: 'Activates speech recognition and transcribes speech to text', 13 | isEnabled: isEnabled, 14 | isHidden: isEnabled, 15 | userFacingName() { 16 | return 'listen' 17 | }, 18 | async call(_, { abortController }) { 19 | // Start dictation using AppleScript 20 | const script = `tell application "System Events" to tell ¬ 21 | (the first process whose frontmost is true) to tell ¬ 22 | menu bar 1 to tell ¬ 23 | menu bar item "Edit" to tell ¬ 24 | menu "Edit" to tell ¬ 25 | menu item "Start Dictation" to ¬ 26 | if exists then click it` 27 | 28 | const { stderr, code } = await execFileNoThrow( 29 | 'osascript', 30 | ['-e', script], 31 | abortController.signal, 32 | ) 33 | 34 | if (code !== 0) { 35 | logError(`Failed to start dictation: ${stderr}`) 36 | return 'Failed to start dictation' 37 | } 38 | return 'Dictation started. Press esc to stop.' 39 | }, 40 | } 41 | 42 | export default listen 43 | -------------------------------------------------------------------------------- /src/services/adapters/base.ts: -------------------------------------------------------------------------------- 1 | import { ModelCapabilities, UnifiedRequestParams, UnifiedResponse } from '../../types/modelCapabilities' 2 | import { ModelProfile } from '../../utils/config' 3 | import { Tool } from '../../Tool' 4 | 5 | export abstract class ModelAPIAdapter { 6 | constructor( 7 | protected capabilities: ModelCapabilities, 8 | protected modelProfile: ModelProfile 9 | ) {} 10 | 11 | // Subclasses must implement these methods 12 | abstract createRequest(params: UnifiedRequestParams): any 13 | abstract parseResponse(response: any): UnifiedResponse 14 | abstract buildTools(tools: Tool[]): any 15 | 16 | // Shared utility methods 17 | protected getMaxTokensParam(): string { 18 | return this.capabilities.parameters.maxTokensField 19 | } 20 | 21 | protected getTemperature(): number { 22 | if (this.capabilities.parameters.temperatureMode === 'fixed_one') { 23 | return 1 24 | } 25 | if (this.capabilities.parameters.temperatureMode === 'restricted') { 26 | return Math.min(1, 0.7) 27 | } 28 | return 0.7 29 | } 30 | 31 | protected shouldIncludeReasoningEffort(): boolean { 32 | return this.capabilities.parameters.supportsReasoningEffort 33 | } 34 | 35 | protected shouldIncludeVerbosity(): boolean { 36 | return this.capabilities.parameters.supportsVerbosity 37 | } 38 | } -------------------------------------------------------------------------------- /src/utils/user.ts: -------------------------------------------------------------------------------- 1 | import { getGlobalConfig, getOrCreateUserID } from './config' 2 | import { memoize } from 'lodash-es' 3 | import { env } from './env' 4 | import { type StatsigUser } from '@statsig/js-client' 5 | import { execFileNoThrow } from './execFileNoThrow' 6 | import { logError, SESSION_ID } from './log' 7 | import { MACRO } from '../constants/macros' 8 | export const getGitEmail = memoize(async (): Promise => { 9 | const result = await execFileNoThrow('git', ['config', 'user.email']) 10 | if (result.code !== 0) { 11 | logError(`Failed to get git email: ${result.stdout} ${result.stderr}`) 12 | return undefined 13 | } 14 | return result.stdout.trim() || undefined 15 | }) 16 | 17 | export const getUser = memoize(async (): Promise => { 18 | const userID = getOrCreateUserID() 19 | const config = getGlobalConfig() 20 | const email = undefined 21 | return { 22 | customIDs: { 23 | // for session level tests 24 | sessionId: SESSION_ID, 25 | }, 26 | userID, 27 | appVersion: MACRO.VERSION, 28 | userAgent: env.platform, 29 | email, 30 | custom: { 31 | nodeVersion: env.nodeVersion, 32 | userType: process.env.USER_TYPE, 33 | organizationUuid: config.oauthAccount?.organizationUuid, 34 | accountUuid: config.oauthAccount?.accountUuid, 35 | }, 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /src/tools/StickerRequestTool/prompt.ts: -------------------------------------------------------------------------------- 1 | export const DESCRIPTION = 2 | 'Sends the user swag stickers with love from Anthropic.' 3 | export const PROMPT = `This tool should be used whenever a user expresses interest in receiving Anthropic or Claude stickers, swag, or merchandise. When triggered, it will display a shipping form for the user to enter their mailing address and contact details. Once submitted, Anthropic will process the request and ship stickers to the provided address. 4 | 5 | Common trigger phrases to watch for: 6 | - "Can I get some Anthropic stickers please?" 7 | - "How do I get Anthropic swag?" 8 | - "I'd love some Claude stickers" 9 | - "Where can I get merchandise?" 10 | - Any mention of wanting stickers or swag 11 | 12 | The tool handles the entire request process by showing an interactive form to collect shipping information. 13 | 14 | NOTE: Only use this tool if the user has explicitly asked us to send or give them stickers. If there are other requests that include the word "sticker", but do not explicitly ask us to send them stickers, do not use this tool. 15 | For example: 16 | - "How do I make custom stickers for my project?" - Do not use this tool 17 | - "I need to store sticker metadata in a database - what schema do you recommend?" - Do not use this tool 18 | - "Show me how to implement drag-and-drop sticker placement with React" - Do not use this tool 19 | ` 20 | -------------------------------------------------------------------------------- /src/components/CostThresholdDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text, useInput } from 'ink' 2 | import React from 'react' 3 | import { Select } from './CustomSelect/select' 4 | import { getTheme } from '../utils/theme' 5 | import Link from './Link' 6 | 7 | interface Props { 8 | onDone: () => void 9 | } 10 | 11 | export function CostThresholdDialog({ onDone }: Props): React.ReactNode { 12 | // Handle Ctrl+C, Ctrl+D and Esc 13 | useInput((input, key) => { 14 | if ((key.ctrl && (input === 'c' || input === 'd')) || key.escape) { 15 | onDone() 16 | } 17 | }) 18 | 19 | return ( 20 | 26 | 27 | 28 | You've spent $5 on AI model API calls this session. 29 | 30 | Learn more about monitoring your AI usage costs: 31 | 32 | 33 | 34 |