shellmate-ai 0.1.0__tar.gz → 0.1.2__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.
Files changed (31) hide show
  1. shellmate_ai-0.1.2/MANIFEST.in +2 -0
  2. shellmate_ai-0.1.2/PKG-INFO +156 -0
  3. shellmate_ai-0.1.2/README.md +142 -0
  4. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/pyproject.toml +4 -3
  5. shellmate_ai-0.1.2/src/shellmate/__init__.py +9 -0
  6. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate/agent.py +16 -4
  7. shellmate_ai-0.1.2/src/shellmate/cli.py +229 -0
  8. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate/config.py +22 -28
  9. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate/context.py +1 -1
  10. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate/privacy.py +43 -4
  11. shellmate_ai-0.1.2/src/shellmate/prompts/Agent.md +162 -0
  12. shellmate_ai-0.1.2/src/shellmate/shellmate.zsh +95 -0
  13. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate/tools/web_search.py +1 -1
  14. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate/zsh_plugin.py +1 -1
  15. shellmate_ai-0.1.2/src/shellmate_ai.egg-info/PKG-INFO +156 -0
  16. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate_ai.egg-info/SOURCES.txt +1 -0
  17. shellmate_ai-0.1.2/src/shellmate_ai.egg-info/entry_points.txt +2 -0
  18. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate_ai.egg-info/requires.txt +1 -0
  19. shellmate_ai-0.1.0/MANIFEST.in +0 -1
  20. shellmate_ai-0.1.0/PKG-INFO +0 -110
  21. shellmate_ai-0.1.0/README.md +0 -97
  22. shellmate_ai-0.1.0/src/shellmate/__init__.py +0 -3
  23. shellmate_ai-0.1.0/src/shellmate/cli.py +0 -159
  24. shellmate_ai-0.1.0/src/shellmate/shellmate.zsh +0 -56
  25. shellmate_ai-0.1.0/src/shellmate_ai.egg-info/PKG-INFO +0 -110
  26. shellmate_ai-0.1.0/src/shellmate_ai.egg-info/entry_points.txt +0 -2
  27. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/LICENSE +0 -0
  28. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/setup.cfg +0 -0
  29. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate/tools/__init__.py +0 -0
  30. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate_ai.egg-info/dependency_links.txt +0 -0
  31. {shellmate_ai-0.1.0 → shellmate_ai-0.1.2}/src/shellmate_ai.egg-info/top_level.txt +0 -0
@@ -0,0 +1,2 @@
1
+ recursive-include src *.zsh
2
+ recursive-include src/shellmate/prompts *.md
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: shellmate-ai
3
+ Version: 0.1.2
4
+ Summary: An AI assistant for your zsh command line
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: langgraph<2.0,>=1.0
9
+ Requires-Dist: langgraph-checkpoint-sqlite<4.0,>=3.0
10
+ Requires-Dist: langchain-openai<2.0,>=1.0
11
+ Requires-Dist: pydantic<3.0,>=2.7
12
+ Requires-Dist: rich<14.0,>=13.0
13
+ Dynamic: license-file
14
+
15
+ # Shellmate
16
+
17
+ 🇨🇳 简体中文 | [🇬🇧 English](README.en.md)
18
+
19
+ [![PyPI](https://img.shields.io/pypi/v/shellmate-ai?color=blue)](https://pypi.org/project/shellmate-ai/)
20
+ [![Python](https://img.shields.io/pypi/pyversions/shellmate-ai.svg)](https://pypi.org/project/shellmate-ai/)
21
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
22
+
23
+ Shellmate 是一个面向 zsh 的命令行 AI 助手。在命令行输入问题后按 **Ctrl-G**,它会结合近期命令历史,用任意 OpenAI 兼容模型(OpenAI、DeepSeek、Qwen 等)给出回答。
24
+
25
+ ## 功能
26
+
27
+ - **Ctrl-G** 快捷键 — 在命令行输入问题后按 Ctrl-G 提问(空缓冲时只提示先输入问题)
28
+ - 终端内渲染 Markdown(代码高亮、表格、列表)
29
+ - 自动捕获上一条命令及其退出码,失败时在提示符上方提示
30
+ - 命令失败后按 **Ctrl-X** 用管道重跑该命令,让 agent 看**完整报错**再解释
31
+ - 近期命令历史作为上下文
32
+ - OpenAI 兼容协议,支持 OpenAI / DeepSeek / Qwen 等
33
+ - 系统提示词可编辑(`~/.config/shellmate/Agent.md`),默认模板随包下发
34
+ - 内置 DuckDuckGo 网页搜索,无需搜索 API Key
35
+ - 模型与搜索请求前自动脱敏:密钥前缀、高熵 token、`KEY=value`、引号包裹的秘密、Webhook URL、URL 内嵌凭据、`curl -u` / `mysql -pXXX` 等命令行开关
36
+ - 本地 SQLite 保存会话记忆(checkpoint),无需数据库服务
37
+
38
+ ## 安装
39
+
40
+ 需要 Python 3.11+。
41
+
42
+ ```sh
43
+ pip install shellmate-ai # 或从源码:pip install -e .
44
+ shellmate-ai init # 创建配置 + Agent 提示词 + zsh 插件 + .zshrc 加载行
45
+ source ~/.zshrc # 或重开终端
46
+ ```
47
+
48
+ > **注**:安装后命令行工具名为 `shellmate-ai`(与 PyPI 包名一致)。
49
+
50
+ ## 使用
51
+
52
+ 在 zsh 中输入问题,然后按 **Ctrl-G**,agent 会结合近期命令历史回答。空缓冲按 Ctrl-G 不会去猜上一条命令的失败原因,只提示你先输入问题 —— 原因是 zsh 没有"命令执行结束"的钩子,命令的输出在提示符重绘时就已经消失,Shellmate 从未捕获过它。想让 agent 看到真正的报错,用 **Ctrl-X** 重跑该命令,或自己接管道。
53
+
54
+ | 触发方式 | agent 能看到什么 |
55
+ | --- | --- |
56
+ | Ctrl-G(命令行有输入) | 你的问题 + 近期命令历史 |
57
+ | Ctrl-G(空缓冲) | 什么都不问,只在终端提示"请先输入问题" |
58
+ | Ctrl-X | 重新执行上一条命令,把 stdout+stderr 合并后的输出交给 agent |
59
+ | `cmd 2>&1 \| shellmate-ai explain` | 同上,但要跑哪条命令由你自己决定 |
60
+ | `shellmate-ai explain-last --last-command CMD --last-exit N` | 只有命令和退出码,**没有输出**,适合脚本;模型被明确告知不要编造报错 |
61
+
62
+ 命令失败时(非零退出码),提示符上方会出现提示:按 **Ctrl-X** 会把上一条命令用 `2>&1 | shellmate-ai explain` 重跑,把合并后的输出交给 agent 解释(重跑可能有副作用,所以由你手动触发)。
63
+
64
+ > **注**:Ctrl-X 任何时候都可用,不限于失败之后;输出过长时只保留末尾 2 万字符。`^X` 同时是 zsh 默认键位的前缀(如 `^X^U` 撤销、`^Xr` 历史搜索),所以 ZLE 会先等待 `KEYTIMEOUT`(默认 0.4 秒)再触发重跑——那些 `^X` 开头的组合键不受影响,觉得迟滞可以自行调小 `KEYTIMEOUT`。另外,绑定 Ctrl-G 会覆盖 zsh 默认的 `send-break`(取消当前命令行);想找回默认行为,可以执行 `bindkey '^G' send-break`,或给插件换个键位。
65
+
66
+ ```sh
67
+ shellmate-ai ask "刚才的命令为什么失败?" # 直接提问
68
+ shellmate-ai ask # 交互式提问
69
+ shellmate-ai ask --history $'ls -la\ngit status' "解释一下" # 手动传历史
70
+ shellmate-ai ask --thread-id my-task "换个新会话" # 指定会话 ID
71
+
72
+ # 把命令输出喂给 Shellmate 解释(管道模式)
73
+ git push origin main 2>&1 | shellmate-ai explain
74
+ tail -200 app.log | shellmate-ai explain "为什么一直报 timeout?"
75
+
76
+ shellmate-ai explain-last --last-command "make build" --last-exit 2 # 只用命令+退出码解释(无输出)
77
+ shellmate-ai config-path # 查看配置路径
78
+ shellmate-ai history-lines # 查看历史条数
79
+ ```
80
+
81
+ ## 配置
82
+
83
+ `shellmate-ai init` 会创建 `~/.config/shellmate/config.json` 和 `Agent.md`。在 config.json 中填入 API Key,或用环境变量设置。
84
+
85
+ `Agent.md` 是系统提示词,可以直接编辑:初始内容来自包内模板 `src/shellmate/prompts/Agent.md`(随 wheel/sdist 一起下发),只在文件不存在时写入,不会覆盖你的修改;改动对新会话生效。
86
+
87
+ ```json
88
+ {
89
+ "llm": { "base_url": "https://api.openai.com/v1", "model": "gpt-4o-mini", "api_key": "", "timeout": 60.0 },
90
+ "shell": { "history_lines": 20 },
91
+ "search": { "endpoint": "https://html.duckduckgo.com/html/" },
92
+ "privacy": { "redact_secrets": true, "redact_high_entropy": true, "custom_patterns": [] }
93
+ }
94
+ ```
95
+
96
+ 字段约束:`llm.timeout` 大于 0 且不超过 600 秒,`shell.history_lines` 为 1–500,`search.endpoint` 必须是 HTTP(S) URL;`privacy.custom_patterns` 是额外的正则,命中后替换为 `[REDACTED]`;关闭 `privacy.redact_secrets` 会同时停用高熵检测。配置中出现未知字段(含已移除的 `provider`、`output_file`、`checkpoint`)会直接报错并指出字段名,不会被静默忽略。
97
+
98
+ 环境变量会覆盖 JSON 配置:
99
+
100
+ | 变量 | 覆盖 |
101
+ | --- | --- |
102
+ | `OPENAI_API_KEY` / `SHELLMATE_API_KEY` | `llm.api_key` |
103
+ | `SHELLMATE_BASE_URL` | `llm.base_url` |
104
+ | `SHELLMATE_MODEL` | `llm.model` |
105
+ | `SHELLMATE_THREAD_ID` | `thread_id`(会话记忆 ID,默认 `shellmate-cli-default`) |
106
+ | `SHELLMATE_SEARCH_ENDPOINT` | `search.endpoint` |
107
+
108
+ CLI 另外会读取 `SHELLMATE_SESSION_ID`(未指定 `--thread-id` 时的会话 ID,由 zsh 插件按窗口设置),以及 `SHELLMATE_HISTORY_TEXT`、`SHELLMATE_LAST_COMMAND`、`SHELLMATE_LAST_EXIT` 作为兜底值 —— 新版插件用 `--history` 参数传历史,另外几个变量只为兼容已安装的旧插件保留。未设置 `HISTFILE` 时,历史文件按 `~/.zsh_history` 读取。
109
+
110
+ ### 运行时文件
111
+
112
+ | 路径 | 说明 |
113
+ | --- | --- |
114
+ | `~/.config/shellmate/config.json` | 配置,权限 600 |
115
+ | `~/.config/shellmate/Agent.md` | 系统提示词,权限 600,可自由编辑 |
116
+ | `~/.config/shellmate/shellmate.zsh` | zsh 插件,由 `shellmate-ai init` 写入,权限 644 |
117
+ | `~/.config/shellmate/data/checkpoints.sqlite` | 会话记忆,SQLite,目录权限 700 |
118
+ | `~/.zshrc` | 由 `init` 幂等追加 `source ~/.config/shellmate/shellmate.zsh` |
119
+
120
+ ## 架构
121
+
122
+ Agent 是一个 LangGraph 状态机:
123
+
124
+ ```mermaid
125
+ flowchart TD
126
+ Start([start]) --> SystemPrompt["system_prompt<br/>加载 Agent.md"]
127
+ SystemPrompt --> Assistant["assistant<br/>调用模型"]
128
+ Assistant -->|需要工具| Tools["tools<br/>网页搜索"]
129
+ Assistant -->|结束| End([end])
130
+ Tools --> Assistant
131
+ ```
132
+
133
+ - **system_prompt** — 某个会话首次执行时读取可编辑的 `Agent.md`,作为系统提示词写入状态(每个会话一次)
134
+ - **assistant** — 把系统提示词置于最前,仅对会话消息脱敏后调用 OpenAI 兼容模型
135
+ - **tools** — 模型请求联网时执行 DuckDuckGo 搜索
136
+
137
+ ## 项目结构
138
+
139
+ ```text
140
+ src/shellmate/
141
+ ├── agent.py # LangGraph agent + SQLite checkpoint
142
+ ├── cli.py # 命令行入口
143
+ ├── config.py # Pydantic 配置 + 读取包内默认提示词
144
+ ├── context.py # 历史格式化
145
+ ├── privacy.py # 脱敏中间件
146
+ ├── zsh_plugin.py # 内置 zsh 插件(从 shellmate.zsh 数据文件读取)
147
+ ├── shellmate.zsh # zsh 插件(Ctrl-G / Ctrl-X / preexec / precmd)
148
+ ├── prompts/
149
+ │ └── Agent.md # 默认系统提示词模板(init 时写入配置目录)
150
+ └── tools/
151
+ └── web_search.py # DuckDuckGo HTML 搜索
152
+ ```
153
+
154
+ ## License
155
+
156
+ [MIT](LICENSE)
@@ -0,0 +1,142 @@
1
+ # Shellmate
2
+
3
+ 🇨🇳 简体中文 | [🇬🇧 English](README.en.md)
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/shellmate-ai?color=blue)](https://pypi.org/project/shellmate-ai/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/shellmate-ai.svg)](https://pypi.org/project/shellmate-ai/)
7
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
+
9
+ Shellmate 是一个面向 zsh 的命令行 AI 助手。在命令行输入问题后按 **Ctrl-G**,它会结合近期命令历史,用任意 OpenAI 兼容模型(OpenAI、DeepSeek、Qwen 等)给出回答。
10
+
11
+ ## 功能
12
+
13
+ - **Ctrl-G** 快捷键 — 在命令行输入问题后按 Ctrl-G 提问(空缓冲时只提示先输入问题)
14
+ - 终端内渲染 Markdown(代码高亮、表格、列表)
15
+ - 自动捕获上一条命令及其退出码,失败时在提示符上方提示
16
+ - 命令失败后按 **Ctrl-X** 用管道重跑该命令,让 agent 看**完整报错**再解释
17
+ - 近期命令历史作为上下文
18
+ - OpenAI 兼容协议,支持 OpenAI / DeepSeek / Qwen 等
19
+ - 系统提示词可编辑(`~/.config/shellmate/Agent.md`),默认模板随包下发
20
+ - 内置 DuckDuckGo 网页搜索,无需搜索 API Key
21
+ - 模型与搜索请求前自动脱敏:密钥前缀、高熵 token、`KEY=value`、引号包裹的秘密、Webhook URL、URL 内嵌凭据、`curl -u` / `mysql -pXXX` 等命令行开关
22
+ - 本地 SQLite 保存会话记忆(checkpoint),无需数据库服务
23
+
24
+ ## 安装
25
+
26
+ 需要 Python 3.11+。
27
+
28
+ ```sh
29
+ pip install shellmate-ai # 或从源码:pip install -e .
30
+ shellmate-ai init # 创建配置 + Agent 提示词 + zsh 插件 + .zshrc 加载行
31
+ source ~/.zshrc # 或重开终端
32
+ ```
33
+
34
+ > **注**:安装后命令行工具名为 `shellmate-ai`(与 PyPI 包名一致)。
35
+
36
+ ## 使用
37
+
38
+ 在 zsh 中输入问题,然后按 **Ctrl-G**,agent 会结合近期命令历史回答。空缓冲按 Ctrl-G 不会去猜上一条命令的失败原因,只提示你先输入问题 —— 原因是 zsh 没有"命令执行结束"的钩子,命令的输出在提示符重绘时就已经消失,Shellmate 从未捕获过它。想让 agent 看到真正的报错,用 **Ctrl-X** 重跑该命令,或自己接管道。
39
+
40
+ | 触发方式 | agent 能看到什么 |
41
+ | --- | --- |
42
+ | Ctrl-G(命令行有输入) | 你的问题 + 近期命令历史 |
43
+ | Ctrl-G(空缓冲) | 什么都不问,只在终端提示"请先输入问题" |
44
+ | Ctrl-X | 重新执行上一条命令,把 stdout+stderr 合并后的输出交给 agent |
45
+ | `cmd 2>&1 \| shellmate-ai explain` | 同上,但要跑哪条命令由你自己决定 |
46
+ | `shellmate-ai explain-last --last-command CMD --last-exit N` | 只有命令和退出码,**没有输出**,适合脚本;模型被明确告知不要编造报错 |
47
+
48
+ 命令失败时(非零退出码),提示符上方会出现提示:按 **Ctrl-X** 会把上一条命令用 `2>&1 | shellmate-ai explain` 重跑,把合并后的输出交给 agent 解释(重跑可能有副作用,所以由你手动触发)。
49
+
50
+ > **注**:Ctrl-X 任何时候都可用,不限于失败之后;输出过长时只保留末尾 2 万字符。`^X` 同时是 zsh 默认键位的前缀(如 `^X^U` 撤销、`^Xr` 历史搜索),所以 ZLE 会先等待 `KEYTIMEOUT`(默认 0.4 秒)再触发重跑——那些 `^X` 开头的组合键不受影响,觉得迟滞可以自行调小 `KEYTIMEOUT`。另外,绑定 Ctrl-G 会覆盖 zsh 默认的 `send-break`(取消当前命令行);想找回默认行为,可以执行 `bindkey '^G' send-break`,或给插件换个键位。
51
+
52
+ ```sh
53
+ shellmate-ai ask "刚才的命令为什么失败?" # 直接提问
54
+ shellmate-ai ask # 交互式提问
55
+ shellmate-ai ask --history $'ls -la\ngit status' "解释一下" # 手动传历史
56
+ shellmate-ai ask --thread-id my-task "换个新会话" # 指定会话 ID
57
+
58
+ # 把命令输出喂给 Shellmate 解释(管道模式)
59
+ git push origin main 2>&1 | shellmate-ai explain
60
+ tail -200 app.log | shellmate-ai explain "为什么一直报 timeout?"
61
+
62
+ shellmate-ai explain-last --last-command "make build" --last-exit 2 # 只用命令+退出码解释(无输出)
63
+ shellmate-ai config-path # 查看配置路径
64
+ shellmate-ai history-lines # 查看历史条数
65
+ ```
66
+
67
+ ## 配置
68
+
69
+ `shellmate-ai init` 会创建 `~/.config/shellmate/config.json` 和 `Agent.md`。在 config.json 中填入 API Key,或用环境变量设置。
70
+
71
+ `Agent.md` 是系统提示词,可以直接编辑:初始内容来自包内模板 `src/shellmate/prompts/Agent.md`(随 wheel/sdist 一起下发),只在文件不存在时写入,不会覆盖你的修改;改动对新会话生效。
72
+
73
+ ```json
74
+ {
75
+ "llm": { "base_url": "https://api.openai.com/v1", "model": "gpt-4o-mini", "api_key": "", "timeout": 60.0 },
76
+ "shell": { "history_lines": 20 },
77
+ "search": { "endpoint": "https://html.duckduckgo.com/html/" },
78
+ "privacy": { "redact_secrets": true, "redact_high_entropy": true, "custom_patterns": [] }
79
+ }
80
+ ```
81
+
82
+ 字段约束:`llm.timeout` 大于 0 且不超过 600 秒,`shell.history_lines` 为 1–500,`search.endpoint` 必须是 HTTP(S) URL;`privacy.custom_patterns` 是额外的正则,命中后替换为 `[REDACTED]`;关闭 `privacy.redact_secrets` 会同时停用高熵检测。配置中出现未知字段(含已移除的 `provider`、`output_file`、`checkpoint`)会直接报错并指出字段名,不会被静默忽略。
83
+
84
+ 环境变量会覆盖 JSON 配置:
85
+
86
+ | 变量 | 覆盖 |
87
+ | --- | --- |
88
+ | `OPENAI_API_KEY` / `SHELLMATE_API_KEY` | `llm.api_key` |
89
+ | `SHELLMATE_BASE_URL` | `llm.base_url` |
90
+ | `SHELLMATE_MODEL` | `llm.model` |
91
+ | `SHELLMATE_THREAD_ID` | `thread_id`(会话记忆 ID,默认 `shellmate-cli-default`) |
92
+ | `SHELLMATE_SEARCH_ENDPOINT` | `search.endpoint` |
93
+
94
+ CLI 另外会读取 `SHELLMATE_SESSION_ID`(未指定 `--thread-id` 时的会话 ID,由 zsh 插件按窗口设置),以及 `SHELLMATE_HISTORY_TEXT`、`SHELLMATE_LAST_COMMAND`、`SHELLMATE_LAST_EXIT` 作为兜底值 —— 新版插件用 `--history` 参数传历史,另外几个变量只为兼容已安装的旧插件保留。未设置 `HISTFILE` 时,历史文件按 `~/.zsh_history` 读取。
95
+
96
+ ### 运行时文件
97
+
98
+ | 路径 | 说明 |
99
+ | --- | --- |
100
+ | `~/.config/shellmate/config.json` | 配置,权限 600 |
101
+ | `~/.config/shellmate/Agent.md` | 系统提示词,权限 600,可自由编辑 |
102
+ | `~/.config/shellmate/shellmate.zsh` | zsh 插件,由 `shellmate-ai init` 写入,权限 644 |
103
+ | `~/.config/shellmate/data/checkpoints.sqlite` | 会话记忆,SQLite,目录权限 700 |
104
+ | `~/.zshrc` | 由 `init` 幂等追加 `source ~/.config/shellmate/shellmate.zsh` |
105
+
106
+ ## 架构
107
+
108
+ Agent 是一个 LangGraph 状态机:
109
+
110
+ ```mermaid
111
+ flowchart TD
112
+ Start([start]) --> SystemPrompt["system_prompt<br/>加载 Agent.md"]
113
+ SystemPrompt --> Assistant["assistant<br/>调用模型"]
114
+ Assistant -->|需要工具| Tools["tools<br/>网页搜索"]
115
+ Assistant -->|结束| End([end])
116
+ Tools --> Assistant
117
+ ```
118
+
119
+ - **system_prompt** — 某个会话首次执行时读取可编辑的 `Agent.md`,作为系统提示词写入状态(每个会话一次)
120
+ - **assistant** — 把系统提示词置于最前,仅对会话消息脱敏后调用 OpenAI 兼容模型
121
+ - **tools** — 模型请求联网时执行 DuckDuckGo 搜索
122
+
123
+ ## 项目结构
124
+
125
+ ```text
126
+ src/shellmate/
127
+ ├── agent.py # LangGraph agent + SQLite checkpoint
128
+ ├── cli.py # 命令行入口
129
+ ├── config.py # Pydantic 配置 + 读取包内默认提示词
130
+ ├── context.py # 历史格式化
131
+ ├── privacy.py # 脱敏中间件
132
+ ├── zsh_plugin.py # 内置 zsh 插件(从 shellmate.zsh 数据文件读取)
133
+ ├── shellmate.zsh # zsh 插件(Ctrl-G / Ctrl-X / preexec / precmd)
134
+ ├── prompts/
135
+ │ └── Agent.md # 默认系统提示词模板(init 时写入配置目录)
136
+ └── tools/
137
+ └── web_search.py # DuckDuckGo HTML 搜索
138
+ ```
139
+
140
+ ## License
141
+
142
+ [MIT](LICENSE)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shellmate-ai"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "An AI assistant for your zsh command line"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -9,10 +9,11 @@ dependencies = [
9
9
  "langgraph-checkpoint-sqlite>=3.0,<4.0",
10
10
  "langchain-openai>=1.0,<2.0",
11
11
  "pydantic>=2.7,<3.0",
12
+ "rich>=13.0,<14.0",
12
13
  ]
13
14
 
14
15
  [project.scripts]
15
- shellmate = "shellmate.cli:main"
16
+ shellmate-ai = "shellmate.cli:main"
16
17
 
17
18
  [build-system]
18
19
  requires = ["setuptools>=68"]
@@ -22,4 +23,4 @@ build-backend = "setuptools.build_meta"
22
23
  where = ["src"]
23
24
 
24
25
  [tool.setuptools.package-data]
25
- shellmate = ["*.zsh"]
26
+ shellmate = ["*.zsh", "prompts/*.md"]
@@ -0,0 +1,9 @@
1
+ """Shellmate:面向 shell 的 AI 助手。"""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("shellmate-ai")
7
+ except PackageNotFoundError:
8
+ # 未通过 pip/uv 安装(如直接运行源码)时回退到占位版本。
9
+ __version__ = "0.1.0"
@@ -77,9 +77,21 @@ class LangGraphAgent:
77
77
  return {"messages": [SystemMessage(content=load_system_prompt())]}
78
78
 
79
79
  def _call_model(self, state: AgentState) -> dict[str, list[AnyMessage]]:
80
- """模型节点:在请求边界脱敏,并将新消息交回图状态。"""
81
- safe_messages = self.privacy.before_model(state["messages"])
82
- response = self.model.invoke(safe_messages)
80
+ """模型节点:对齐消息顺序、在请求边界脱敏,再调用模型。
81
+
82
+ 两处必要处理:
83
+
84
+ 1. ``system_prompt`` 节点是在用户消息之后写入状态的,直接发送会让系统提示词
85
+ 排在用户消息后面;部分 OpenAI 兼容服务会拒绝或忽略这种情况,因此这里把
86
+ 系统提示词提到最前。
87
+ 2. 系统提示词来自用户本地编辑的 ``Agent.md``,属于可信输入,且脱敏规则会破坏
88
+ 其中的命令示例(如 ``KEY=value``),所以不经过脱敏,只有对话消息需要脱敏。
89
+ """
90
+ messages = state["messages"]
91
+ system = [message for message in messages if isinstance(message, SystemMessage)]
92
+ conversation = [message for message in messages if not isinstance(message, SystemMessage)]
93
+ safe_messages = self.privacy.before_model(conversation)
94
+ response = self.model.invoke([*system, *safe_messages])
83
95
  return {"messages": [response]}
84
96
 
85
97
  def _wrap_tool_call(self, request, execute):
@@ -138,5 +150,5 @@ class LangGraphAgent:
138
150
  last_message = result["messages"][-1]
139
151
  content = last_message.content
140
152
  if isinstance(content, str):
141
- return content or "(No response content.)"
153
+ return content or "(模型没有返回内容。)"
142
154
  return "\n".join(str(block.get("text", block)) for block in content)
@@ -0,0 +1,229 @@
1
+ """Shellmate 命令行入口,负责解析参数并调用相应模块。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import itertools
7
+ import os
8
+ import sys
9
+ import threading
10
+
11
+ from rich.console import Console
12
+ from rich.markdown import Markdown
13
+
14
+ from shellmate import __version__
15
+ from shellmate.agent import AgentError, LangGraphAgent
16
+ from shellmate.config import (
17
+ AGENT_PROMPT_PATH,
18
+ CONFIG_PATH,
19
+ DATA_DIR,
20
+ ZSH_PLUGIN_PATH,
21
+ ensure_data_dir,
22
+ ensure_zsh_plugin,
23
+ load_config,
24
+ )
25
+ from shellmate.context import ShellContext, read_context, read_zsh_history
26
+
27
+ # 管道模式输入的最大保留字符数;报错信息通常在输出末尾,故保留尾部。
28
+ _MAX_OUTPUT_CHARS = 20000
29
+
30
+
31
+ def _history_text(config, args) -> str:
32
+ """历史来源优先级:zsh 插件经 --history 传入 > 环境变量 > 历史文件兜底。"""
33
+ return (
34
+ getattr(args, "history", "")
35
+ or os.environ.get("SHELLMATE_HISTORY_TEXT", "")
36
+ or read_zsh_history(config.shell.history_lines)
37
+ )
38
+
39
+
40
+ def _thread_id(config, args) -> str:
41
+ """会话 ID 来源优先级:--thread-id > zsh 会话环境变量 > 配置兜底。"""
42
+ return (
43
+ getattr(args, "thread_id", None)
44
+ or os.environ.get("SHELLMATE_SESSION_ID")
45
+ or config.thread_id
46
+ )
47
+
48
+
49
+ _SPINNER_CHARS = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
50
+
51
+
52
+ class _Spinner:
53
+ """在 stderr 上显示单行动态加载态,退出时清除该行。
54
+
55
+ 用 ``\\r`` 回到行首再覆盖,不用多行光标移动,避免在 zsh/zle 等环境
56
+ 出现重复或残留。
57
+ """
58
+
59
+ def __init__(self, message: str) -> None:
60
+ self._message = message
61
+ self._stop = threading.Event()
62
+ self._thread = threading.Thread(target=self._run, daemon=True)
63
+
64
+ def _run(self) -> None:
65
+ for char in itertools.cycle(_SPINNER_CHARS):
66
+ if self._stop.is_set():
67
+ break
68
+ sys.stderr.write(f"\r{char} {self._message}")
69
+ sys.stderr.flush()
70
+ self._stop.wait(0.08)
71
+
72
+ def __enter__(self) -> "_Spinner":
73
+ self._thread.start()
74
+ return self
75
+
76
+ def __exit__(self, *exc: object) -> None:
77
+ self._stop.set()
78
+ self._thread.join(timeout=1)
79
+ sys.stderr.write("\r\x1b[K")
80
+ sys.stderr.flush()
81
+
82
+
83
+ def _ask(config, question: str, context: ShellContext, args) -> None:
84
+ """运行 agent 并打印回答,统一异常处理。
85
+
86
+ 生成期间在 stderr 显示动态加载态;TTY 下用 rich 一次性渲染 Markdown,
87
+ 非 TTY(管道/重定向)则输出纯 Markdown 文本。
88
+ """
89
+ try:
90
+ agent = LangGraphAgent(config)
91
+ to_tty = sys.stdout.isatty()
92
+ if sys.stderr.isatty():
93
+ with _Spinner(f"正在请求 {config.llm.model} …"):
94
+ result = agent.ask(question, context, _thread_id(config, args))
95
+ else:
96
+ result = agent.ask(question, context, _thread_id(config, args))
97
+ if to_tty:
98
+ Console().print(Markdown(result))
99
+ else:
100
+ print(result)
101
+ except (AgentError, ValueError, OSError) as exc:
102
+ print(f"shellmate-ai: {exc}", file=sys.stderr)
103
+ raise SystemExit(1) from exc
104
+
105
+
106
+ def main() -> None:
107
+ """处理初始化、提问、解释上一条命令与配置查看等子命令。"""
108
+ parser = argparse.ArgumentParser(prog="shellmate-ai", description="结合当前 shell 会话向 AI 提问")
109
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
110
+ sub = parser.add_subparsers(dest="command")
111
+ ask_parser = sub.add_parser("ask", help="结合近期命令历史提问")
112
+ ask_parser.add_argument("question", nargs="*", help="要问的问题;多个词会用空格连接")
113
+ ask_parser.add_argument("--history", default="", help="近期命令历史,由 zsh 插件传入")
114
+ ask_parser.add_argument("--thread-id", help="LangGraph 会话 ID,通常由 zsh 传入")
115
+ explain_parser = sub.add_parser(
116
+ "explain",
117
+ help="解释从标准输入读到的命令输出(如 cmd 2>&1 | shellmate-ai explain)",
118
+ )
119
+ explain_parser.add_argument("question", nargs="*", help="可选问题;默认总结这段输出")
120
+ explain_parser.add_argument("--history", default="", help="近期命令历史,由 zsh 插件传入")
121
+ explain_parser.add_argument("--thread-id", help="LangGraph 会话 ID,通常由 zsh 传入")
122
+ last_parser = sub.add_parser(
123
+ "explain-last",
124
+ help="解释指定的上一条命令及其退出码(需 --last-command/--last-exit,供脚本手动调用)",
125
+ )
126
+ last_parser.add_argument("--history", default="", help="近期命令历史,由 zsh 插件传入")
127
+ last_parser.add_argument("--thread-id", help="LangGraph 会话 ID,通常由 zsh 传入")
128
+ last_parser.add_argument("--last-command", default="", help="上一条命令,由 zsh 插件传入")
129
+ last_parser.add_argument("--last-exit", default="", help="上一条命令的退出码,由 zsh 插件传入")
130
+ sub.add_parser("init", help="创建本地配置、Agent 提示词、数据目录和 zsh 插件")
131
+ sub.add_parser("config-path", help="打印配置文件路径")
132
+ sub.add_parser("history-lines", help="打印配置的历史条数")
133
+ args = parser.parse_args()
134
+ try:
135
+ config = load_config()
136
+ except (ValueError, OSError) as exc:
137
+ print(f"shellmate-ai: {exc}", file=sys.stderr)
138
+ raise SystemExit(2) from exc
139
+
140
+ if args.command == "init":
141
+ ensure_data_dir()
142
+ zshrc_updated = ensure_zsh_plugin()
143
+ print(f"已创建或确认配置文件:{CONFIG_PATH}")
144
+ print(f"已创建或确认 Agent 提示词:{AGENT_PROMPT_PATH}")
145
+ print(f"已创建或确认数据目录:{DATA_DIR}")
146
+ print(f"已创建或确认 zsh 插件:{ZSH_PLUGIN_PATH}")
147
+ if zshrc_updated:
148
+ print("已在 ~/.zshrc 添加插件加载行,请执行 `source ~/.zshrc` 使其生效。")
149
+ else:
150
+ print("~/.zshrc 已包含插件加载行,无需修改。")
151
+ return
152
+ if args.command == "config-path":
153
+ print(CONFIG_PATH)
154
+ return
155
+ if args.command == "history-lines":
156
+ print(config.shell.history_lines)
157
+ return
158
+
159
+ if args.command == "explain-last":
160
+ # 优先使用插件通过参数传入的值;环境变量仅作为旧版插件的兜底,
161
+ # 因为导出的环境变量会被之后启动的所有子进程继承。
162
+ last_command = (args.last_command or os.environ.get("SHELLMATE_LAST_COMMAND", "")).strip()
163
+ if not last_command:
164
+ print("shellmate-ai: 没有可解释的上一条命令。", file=sys.stderr)
165
+ raise SystemExit(1)
166
+ raw_exit = (args.last_exit or os.environ.get("SHELLMATE_LAST_EXIT", "")).strip()
167
+ try:
168
+ exit_code = int(raw_exit)
169
+ except ValueError:
170
+ exit_code = None
171
+ history = _history_text(config, args)
172
+ context = ShellContext(history=history, last_command=last_command, last_exit_code=exit_code)
173
+ # zsh 没有 postexec 钩子,命令输出在 precmd 执行时已经消失,插件无法事后捕获,
174
+ # 这里能给的只有命令本身、退出码和历史。必须明确告诉模型"没有输出",否则它会
175
+ # 顺着"解释失败原因"的提问编造一段看起来合理的报错。
176
+ if exit_code == 0:
177
+ question = (
178
+ "解释一下刚才这条命令:它做了什么、有什么副作用或值得注意的地方。"
179
+ "注意:没有捕获到它的输出,不要假定或编造输出内容;如需确认,请给出"
180
+ "获取输出的命令(例如 `cmd 2>&1 | shellmate-ai explain`)。"
181
+ )
182
+ else:
183
+ question = (
184
+ "刚才这条命令失败了,但没有捕获到它的输出,目前只有命令本身和退出码。"
185
+ "请结合退出码说明最可能的原因、还需要哪些输出才能确认,并给出获取该输出的"
186
+ "具体命令(例如 `cmd 2>&1 | shellmate-ai explain`)。不要编造具体的报错内容。"
187
+ )
188
+ _ask(config, question, context, args)
189
+ return
190
+
191
+ if args.command == "explain":
192
+ if sys.stdin.isatty():
193
+ print(
194
+ "shellmate-ai: explain 需要管道输入,例如:command 2>&1 | shellmate-ai explain",
195
+ file=sys.stderr,
196
+ )
197
+ raise SystemExit(1)
198
+ output = sys.stdin.read()
199
+ if not output.strip():
200
+ print("shellmate-ai: 标准输入为空。", file=sys.stderr)
201
+ raise SystemExit(1)
202
+ output = output[-_MAX_OUTPUT_CHARS:]
203
+ question = " ".join(args.question).strip() or "解释这段命令输出:发生了什么、是否报错、以及该如何处理。"
204
+ history = _history_text(config, args)
205
+ context = ShellContext(history=history, output=output)
206
+ _ask(config, question, context, args)
207
+ return
208
+
209
+ if args.command != "ask":
210
+ parser.print_help()
211
+ return
212
+
213
+ if args.question:
214
+ question = " ".join(args.question).strip()
215
+ else:
216
+ try:
217
+ question = input("向 Shellmate 提问: ").strip()
218
+ except (EOFError, KeyboardInterrupt):
219
+ print("\nshellmate-ai: 未输入问题。", file=sys.stderr)
220
+ raise SystemExit(1)
221
+ if not question:
222
+ print("shellmate-ai: 未输入问题。", file=sys.stderr)
223
+ raise SystemExit(1)
224
+ context = read_context(_history_text(config, args))
225
+ _ask(config, question, context, args)
226
+
227
+
228
+ if __name__ == "__main__":
229
+ main()