kittycode 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .env
10
+ .venv/
11
+ venv/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .DS_Store
15
+ dist/
16
+ .pytest_cache/
17
+ findings.md
18
+ progress.md
19
+ task_plan.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jimmy Ye
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: kittycode
3
+ Version: 0.1.0
4
+ Summary: Minimal AI coding agent with a simple tool loop.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: anthropic>=0.84.0
9
+ Requires-Dist: openai>=1.0
10
+ Requires-Dist: prompt-toolkit>=3.0
11
+ Requires-Dist: rich>=13.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == 'dev'
@@ -0,0 +1,186 @@
1
+ # KittyCode
2
+
3
+ KittyCode is a minimal terminal coding agent focused on a compact, readable implementation. It keeps the core runtime straightforward, with an agent loop, local tools, context compression, a small command-line interface, and support for both OpenAI-compatible and Anthropic APIs.
4
+
5
+ ## Background
6
+
7
+ KittyCode follows a simple terminal-agent runtime model:
8
+
9
+ - A user message is sent through the configured model interface.
10
+ - The model can either answer directly or call tools.
11
+ - Tool calls are executed locally and the results are fed back into the conversation.
12
+ - The loop continues until the model returns plain text.
13
+
14
+ The project is intentionally small. It includes the core agent runtime, a compact CLI, session persistence, context compression, and a default built-in tool set.
15
+
16
+ ## Features
17
+
18
+ - Minimal agent loop with optional parallel execution for multiple tool calls.
19
+ - LLM adapter that supports both OpenAI-compatible and Anthropic interfaces.
20
+ - Built-in tools for shell commands, file reading, file writing, targeted editing, glob search, regex search, and sub-agents.
21
+ - Startup skill discovery from `~/.kittycode/skills`, with skill metadata injected into the system prompt each round.
22
+ - Interactive REPL and one-shot command mode.
23
+ - Context compression to keep long sessions manageable.
24
+ - Session save and resume support.
25
+
26
+ ## Requirements
27
+
28
+ - Python 3.10 or newer
29
+ - An API key for either an OpenAI-compatible endpoint or an Anthropic-compatible endpoint
30
+
31
+ ## Installation
32
+
33
+ Clone the repository and install it in editable mode:
34
+
35
+ ```bash
36
+ cd KittyCode
37
+ python -m pip install -e .
38
+ ```
39
+
40
+ If you also want the development test dependency:
41
+
42
+ ```bash
43
+ python -m pip install -e .[dev]
44
+ ```
45
+
46
+ ## Configuration
47
+
48
+ KittyCode reads startup configuration from `~/.kittycode/config.json`.
49
+
50
+ Supported fields:
51
+
52
+ - `interface`: `openai` or `anthropic`
53
+ - `api_key`
54
+ - `model`
55
+ - `base_url`
56
+ - `max_tokens`
57
+ - `temperature`
58
+ - `max_context`
59
+
60
+ OpenAI-compatible example:
61
+
62
+ ```json
63
+ {
64
+ "interface": "openai",
65
+ "api_key": "sk-...",
66
+ "model": "gpt-4o",
67
+ "base_url": "https://api.openai.com/v1",
68
+ "max_tokens": 4096,
69
+ "temperature": 0,
70
+ "max_context": 128000
71
+ }
72
+ ```
73
+
74
+ Anthropic example:
75
+
76
+ ```json
77
+ {
78
+ "interface": "anthropic",
79
+ "api_key": "sk-ant-...",
80
+ "model": "claude-3-7-sonnet-latest",
81
+ "base_url": "https://api.anthropic.com",
82
+ "max_tokens": 4096,
83
+ "temperature": 0,
84
+ "max_context": 128000
85
+ }
86
+ ```
87
+
88
+ The CLI still allows explicit overrides such as `--model`, `--interface`, `--base-url`, and `--api-key`.
89
+
90
+ ## Skills
91
+
92
+ At startup, KittyCode scans `~/.kittycode/skills` for skill folders. Each skill should live in its own directory and include a `SKILL.md` file at the top level.
93
+
94
+ Expected layout:
95
+
96
+ ```text
97
+ ~/.kittycode/skills/
98
+ example-skill/
99
+ SKILL.md
100
+ other-files...
101
+ ```
102
+
103
+ KittyCode reads the leading `name` and `description` fields from each `SKILL.md`, keeps the resulting skill list in memory, and inserts the list into the system prompt at the start of every round. The prompt includes:
104
+
105
+ - `name`
106
+ - `description`
107
+ - `path`
108
+
109
+ This allows the model to see which local skills are available and decide when to read and use them.
110
+
111
+ Before each round, KittyCode checks whether the skill directory changed and reloads the cached skill metadata when needed, so adding or editing skills does not require restarting the process.
112
+
113
+ You can also invoke a loaded skill directly from the CLI with `/<skill name>`.
114
+
115
+ - `/<skill name>` selects that skill for your next non-command message.
116
+ - `/<skill name> <task>` runs the next request immediately with that skill.
117
+
118
+ ## Usage
119
+
120
+ Run the interactive terminal UI:
121
+
122
+ ```bash
123
+ kittycode
124
+ ```
125
+
126
+ You can also use the module entry point:
127
+
128
+ ```bash
129
+ python -m kittycode
130
+ ```
131
+
132
+ Run a one-shot prompt and exit:
133
+
134
+ ```bash
135
+ kittycode -p "Explain the project structure"
136
+ ```
137
+
138
+ Resume a saved session:
139
+
140
+ ```bash
141
+ kittycode -r session_1234567890
142
+ ```
143
+
144
+ Override model, interface, or endpoint from the command line:
145
+
146
+ ```bash
147
+ kittycode --interface anthropic --model claude-3-7-sonnet-latest
148
+ ```
149
+
150
+ ## Interactive Commands
151
+
152
+ Inside the REPL, KittyCode supports:
153
+
154
+ - `/help`
155
+ - `/reset`
156
+ - `/skills`
157
+ - `/<skill name>`
158
+ - `/model <name>`
159
+ - `/tokens`
160
+ - `/compact`
161
+ - `/save`
162
+ - `/sessions`
163
+ - `/quit`
164
+
165
+ The `/skills` command refreshes the local skill cache if the skill directory has changed and then prints the currently loaded skills.
166
+ Slash commands also support prefix matching while typing, so entering `/` shows matching commands and skills through completion suggestions.
167
+
168
+ ## Project Layout
169
+
170
+ - `kittycode/agent.py`: core agent loop
171
+ - `kittycode/llm.py`: streaming LLM wrapper
172
+ - `kittycode/context.py`: context compression
173
+ - `kittycode/cli.py`: interactive and one-shot CLI
174
+ - `kittycode/session.py`: session persistence
175
+ - `kittycode/tools/`: built-in tools
176
+ - `tests/`: focused runtime and tool tests
177
+
178
+ ## Development
179
+
180
+ Run the test suite:
181
+
182
+ ```bash
183
+ python -m pytest -q
184
+ ```
185
+
186
+ The current test suite covers the exported API, config-file behavior, provider conversion helpers, context compression, session helpers, the default tool registry, and skill discovery/prompt injection.
@@ -0,0 +1,186 @@
1
+ # KittyCode
2
+
3
+ KittyCode 是一个运行在终端里的轻量级 AI 编程代理,目标是在尽量精简代码的前提下保留清晰、直接的核心运行逻辑,包括主代理循环、本地工具调用、上下文压缩、命令行交互,以及对 OpenAI 兼容接口和 Anthropic 接口的支持。
4
+
5
+ ## 项目背景
6
+
7
+ KittyCode 的基本工作方式如下:
8
+
9
+ - 用户输入先通过当前配置的模型接口发送出去。
10
+ - 模型可以直接回答,也可以发起工具调用。
11
+ - 本地执行工具后,再把结果回填给模型继续推理。
12
+ - 直到模型返回普通文本,当前任务才结束。
13
+
14
+ 这个项目刻意保持小而清晰,只保留最核心的运行时能力,包括代理主循环、命令行界面、会话保存、上下文压缩,以及默认工具集。
15
+
16
+ ## 功能特点
17
+
18
+ - 保留核心 agent loop,并支持多工具并行执行。
19
+ - LLM 适配层同时支持 OpenAI 兼容接口和 Anthropic 接口。
20
+ - 内置 Bash、读文件、写文件、精确替换编辑、Glob 搜索、Grep 搜索、子代理等工具。
21
+ - 启动时自动扫描 `~/.kittycode/skills`,并在每轮系统 prompt 开头注入可用 skill 列表。
22
+ - 支持交互式 REPL 和单次命令模式。
23
+ - 支持长会话上下文压缩。
24
+ - 支持保存和恢复历史会话。
25
+
26
+ ## 环境要求
27
+
28
+ - Python 3.10 及以上
29
+ - OpenAI 兼容接口或 Anthropic 接口所需的 API Key
30
+
31
+ ## 安装方法
32
+
33
+ 进入项目目录后,以可编辑模式安装:
34
+
35
+ ```bash
36
+ cd KittyCode
37
+ python -m pip install -e .
38
+ ```
39
+
40
+ 如果还要安装测试依赖:
41
+
42
+ ```bash
43
+ python -m pip install -e .[dev]
44
+ ```
45
+
46
+ ## 配置说明
47
+
48
+ KittyCode 启动时会从 `~/.kittycode/config.json` 读取配置。
49
+
50
+ 支持的字段包括:
51
+
52
+ - `interface`:`openai` 或 `anthropic`
53
+ - `api_key`
54
+ - `model`
55
+ - `base_url`
56
+ - `max_tokens`
57
+ - `temperature`
58
+ - `max_context`
59
+
60
+ OpenAI 兼容接口示例:
61
+
62
+ ```json
63
+ {
64
+ "interface": "openai",
65
+ "api_key": "sk-...",
66
+ "model": "gpt-4o",
67
+ "base_url": "https://api.openai.com/v1",
68
+ "max_tokens": 4096,
69
+ "temperature": 0,
70
+ "max_context": 128000
71
+ }
72
+ ```
73
+
74
+ Anthropic 接口示例:
75
+
76
+ ```json
77
+ {
78
+ "interface": "anthropic",
79
+ "api_key": "sk-ant-...",
80
+ "model": "claude-3-7-sonnet-latest",
81
+ "base_url": "https://api.anthropic.com",
82
+ "max_tokens": 4096,
83
+ "temperature": 0,
84
+ "max_context": 128000
85
+ }
86
+ ```
87
+
88
+ 如果需要,也可以通过命令行参数临时覆盖这些配置,例如 `--model`、`--interface`、`--base-url`、`--api-key`。
89
+
90
+ ## Skills 机制
91
+
92
+ KittyCode 启动时会扫描 `~/.kittycode/skills` 目录。每个 skill 使用一个独立文件夹,文件夹顶层需要有一个 `SKILL.md`。
93
+
94
+ 目录结构示例:
95
+
96
+ ```text
97
+ ~/.kittycode/skills/
98
+ example-skill/
99
+ SKILL.md
100
+ other-files...
101
+ ```
102
+
103
+ 运行时会从每个 `SKILL.md` 开头读取 `name` 和 `description`,并把得到的 skill 列表缓存在进程内存中。每一轮对话时,系统 prompt 最前面都会包含以下字段:
104
+
105
+ - `name`
106
+ - `description`
107
+ - `path`
108
+
109
+ 这样模型就能先看到本地有哪些 skill,再按需读取对应目录下的 `SKILL.md` 和其他相关文件。
110
+
111
+ 每轮对话前,KittyCode 都会检查 skill 目录是否发生变化;如果新增或修改了 skill,会自动刷新内存缓存,因此不需要重启进程。
112
+
113
+ 你也可以在 CLI 中直接通过 `/<skill 名称>` 使用某个已加载的 skill。
114
+
115
+ - `/<skill 名称>`:选中该 skill,下一条普通输入会自动带上这个 skill。
116
+ - `/<skill 名称> <任务>`:立即用这个 skill 执行当前请求。
117
+
118
+ ## 使用方法
119
+
120
+ 启动交互式终端界面:
121
+
122
+ ```bash
123
+ kittycode
124
+ ```
125
+
126
+ 也可以直接通过模块启动:
127
+
128
+ ```bash
129
+ python -m kittycode
130
+ ```
131
+
132
+ 单次执行一个提示词并退出:
133
+
134
+ ```bash
135
+ kittycode -p "Explain the project structure"
136
+ ```
137
+
138
+ 恢复历史会话:
139
+
140
+ ```bash
141
+ kittycode -r session_1234567890
142
+ ```
143
+
144
+ 临时覆盖模型、接口类型或接口地址:
145
+
146
+ ```bash
147
+ kittycode --interface anthropic --model claude-3-7-sonnet-latest
148
+ ```
149
+
150
+ ## 交互命令
151
+
152
+ 在 REPL 中可用的命令有:
153
+
154
+ - `/help`
155
+ - `/reset`
156
+ - `/skills`
157
+ - `/<skill 名称>`
158
+ - `/model <name>`
159
+ - `/tokens`
160
+ - `/compact`
161
+ - `/save`
162
+ - `/sessions`
163
+ - `/quit`
164
+
165
+ `/skills` 命令会在检测到 skill 目录变化时刷新本地缓存,并输出当前已加载的 skill 列表。
166
+ 当输入以 `/` 开头时,CLI 还会根据前缀自动补全可用命令和 skill。
167
+
168
+ ## 目录结构
169
+
170
+ - `kittycode/agent.py`:核心代理循环
171
+ - `kittycode/llm.py`:流式 LLM 封装
172
+ - `kittycode/context.py`:上下文压缩
173
+ - `kittycode/cli.py`:交互式与单次命令入口
174
+ - `kittycode/session.py`:会话持久化
175
+ - `kittycode/tools/`:内置工具集合
176
+ - `tests/`:核心运行时与工具测试
177
+
178
+ ## 开发与测试
179
+
180
+ 运行测试:
181
+
182
+ ```bash
183
+ python -m pytest -q
184
+ ```
185
+
186
+ 当前测试主要覆盖导出 API、config.json 读取、provider 转换辅助逻辑、上下文压缩、会话辅助函数、默认工具注册表,以及 skill 发现与 prompt 注入逻辑。
@@ -0,0 +1,10 @@
1
+ """KittyCode - minimal AI coding agent."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from kittycode.agent import Agent
6
+ from kittycode.config import Config
7
+ from kittycode.llm import LLM
8
+ from kittycode.tools import ALL_TOOLS
9
+
10
+ __all__ = ["Agent", "Config", "LLM", "ALL_TOOLS", "__version__"]
@@ -0,0 +1,3 @@
1
+ from kittycode.cli import main
2
+
3
+ main()
@@ -0,0 +1,125 @@
1
+ """Core agent loop.
2
+
3
+ This is the heart of KittyCode.
4
+
5
+ user message -> LLM (with tools) -> tool calls? -> execute -> loop
6
+ -> text reply? -> return to user
7
+
8
+ It keeps looping until the LLM responds with plain text, which means it is
9
+ done working and ready to report back.
10
+ """
11
+
12
+ import concurrent.futures
13
+
14
+ from .context import ContextManager
15
+ from .llm import LLM
16
+ from .prompt import system_prompt
17
+ from .skills import load_skills
18
+ from .tools import ALL_TOOLS, get_tool
19
+ from .tools.agent import AgentTool
20
+ from .tools.base import Tool
21
+
22
+
23
+ class Agent:
24
+ def __init__(
25
+ self,
26
+ llm: LLM,
27
+ tools: list[Tool] | None = None,
28
+ max_context_tokens: int = 128_000,
29
+ max_rounds: int = 50,
30
+ ):
31
+ self.llm = llm
32
+ self.tools = tools if tools is not None else ALL_TOOLS
33
+ self.messages: list[dict] = []
34
+ self.context = ContextManager(max_tokens=max_context_tokens)
35
+ self.max_rounds = max_rounds
36
+ self.skills = []
37
+ self._system = ""
38
+ self.refresh_skills(force_reload=True)
39
+
40
+ for tool in self.tools:
41
+ if isinstance(tool, AgentTool):
42
+ tool._parent_agent = self
43
+
44
+ def _full_messages(self) -> list[dict]:
45
+ self.refresh_skills()
46
+ return [{"role": "system", "content": self._system}] + self.messages
47
+
48
+ def _tool_schemas(self) -> list[dict]:
49
+ return [tool.schema() for tool in self.tools]
50
+
51
+ def chat(self, user_input: str, on_token=None, on_tool=None) -> str:
52
+ """Process one user message. May involve multiple LLM/tool rounds."""
53
+ self.messages.append({"role": "user", "content": user_input})
54
+ self.context.maybe_compress(self.messages, self.llm)
55
+
56
+ for _ in range(self.max_rounds):
57
+ response = self.llm.chat(
58
+ messages=self._full_messages(),
59
+ tools=self._tool_schemas(),
60
+ on_token=on_token,
61
+ )
62
+
63
+ if not response.tool_calls:
64
+ self.messages.append(response.message)
65
+ return response.content
66
+
67
+ self.messages.append(response.message)
68
+
69
+ if len(response.tool_calls) == 1:
70
+ tool_call = response.tool_calls[0]
71
+ if on_tool:
72
+ on_tool(tool_call.name, tool_call.arguments)
73
+ result = self._exec_tool(tool_call)
74
+ self.messages.append(
75
+ {
76
+ "role": "tool",
77
+ "tool_call_id": tool_call.id,
78
+ "content": result,
79
+ }
80
+ )
81
+ else:
82
+ results = self._exec_tools_parallel(response.tool_calls, on_tool)
83
+ for tool_call, result in zip(response.tool_calls, results):
84
+ self.messages.append(
85
+ {
86
+ "role": "tool",
87
+ "tool_call_id": tool_call.id,
88
+ "content": result,
89
+ }
90
+ )
91
+
92
+ self.context.maybe_compress(self.messages, self.llm)
93
+
94
+ return "(reached maximum tool-call rounds)"
95
+
96
+ def _exec_tool(self, tool_call) -> str:
97
+ """Execute a single tool call and return the result string."""
98
+ tool = get_tool(tool_call.name)
99
+ if tool is None:
100
+ return f"Error: unknown tool '{tool_call.name}'"
101
+ try:
102
+ return tool.execute(**tool_call.arguments)
103
+ except TypeError as exc:
104
+ return f"Error: bad arguments for {tool_call.name}: {exc}"
105
+ except Exception as exc:
106
+ return f"Error executing {tool_call.name}: {exc}"
107
+
108
+ def _exec_tools_parallel(self, tool_calls, on_tool=None) -> list[str]:
109
+ """Run multiple tool calls concurrently using threads."""
110
+ for tool_call in tool_calls:
111
+ if on_tool:
112
+ on_tool(tool_call.name, tool_call.arguments)
113
+
114
+ with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
115
+ futures = [pool.submit(self._exec_tool, tool_call) for tool_call in tool_calls]
116
+ return [future.result() for future in futures]
117
+
118
+ def reset(self):
119
+ """Clear conversation history."""
120
+ self.messages.clear()
121
+
122
+ def refresh_skills(self, force_reload: bool = False):
123
+ """Refresh cached skill metadata and rebuild the system prompt."""
124
+ self.skills = load_skills(force_reload=force_reload)
125
+ self._system = system_prompt(self.tools, self.skills)