shellmate-ai 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xuliji
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 @@
1
+ recursive-include src *.zsh
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: shellmate-ai
3
+ Version: 0.1.0
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
+ Dynamic: license-file
13
+
14
+ # Shellmate
15
+
16
+ 🇨🇳 简体中文 | [🇬🇧 English](README.en.md)
17
+
18
+ Shellmate 是一个面向 zsh 的命令行 AI 助手。在命令行输入问题后按 **Ctrl-G**,它会结合近期命令历史,用任意 OpenAI 兼容模型(OpenAI、DeepSeek、Qwen 等)给出回答。
19
+
20
+ ## 功能
21
+
22
+ - **Ctrl-G** 快捷键 — 输入问题按 Ctrl-G 提问;**空缓冲按 Ctrl-G 自动解释上一条命令**
23
+ - 自动捕获上一条命令及其退出码,失败时结合退出码定位原因
24
+ - 近期命令历史作为上下文
25
+ - OpenAI 兼容协议,支持 OpenAI / DeepSeek / Qwen 等
26
+ - 内置 DuckDuckGo 网页搜索,无需搜索 API Key
27
+ - 模型与搜索请求前自动脱敏(含高熵密钥检测)
28
+ - 本地 SQLite 保存 checkpoint,无需数据库服务
29
+
30
+ ## 安装
31
+
32
+ 需要 Python 3.11+。
33
+
34
+ ```sh
35
+ pip install shellmate # 或从源码:pip install -e .
36
+ shellmate init # 创建配置 + zsh 插件 + .zshrc 加载行
37
+ source ~/.zshrc # 或重开终端
38
+ ```
39
+
40
+ ## 使用
41
+
42
+ 在 zsh 中输入问题,然后按 **Ctrl-G**。**空缓冲按 Ctrl-G**(命令行没有输入内容)会自动结合上一条命令及其退出码,解释它为什么失败。
43
+
44
+ ```sh
45
+ shellmate ask "刚才的命令为什么失败?" # 直接提问
46
+ shellmate ask # 交互式提问
47
+ shellmate ask --history $'ls -la\ngit status' "..." # 手动传历史
48
+
49
+ # 把命令输出喂给 Shellmate 解释(管道模式)
50
+ git push origin main 2>&1 | shellmate explain
51
+ tail -200 app.log | shellmate explain "为什么一直报 timeout?"
52
+
53
+ shellmate explain-last # 解释上一条命令(Ctrl-G 空缓冲触发)
54
+ shellmate config-path # 查看配置路径
55
+ shellmate history-lines # 查看历史条数
56
+ ```
57
+
58
+ ## 配置
59
+
60
+ `shellmate init` 会创建 `~/.config/shellmate/config.json` 和 `Agent.md`。在 config.json 中填入 API Key,或用环境变量设置。
61
+
62
+ ```json
63
+ {
64
+ "llm": { "base_url": "https://api.openai.com/v1", "model": "gpt-4o-mini", "api_key": "" },
65
+ "shell": { "history_lines": 20 },
66
+ "search": { "endpoint": "https://html.duckduckgo.com/html/" },
67
+ "privacy": { "redact_secrets": true, "redact_high_entropy": true, "custom_patterns": [] }
68
+ }
69
+ ```
70
+
71
+ 环境变量会覆盖 JSON 配置:
72
+
73
+ | 变量 | 覆盖 |
74
+ | --- | --- |
75
+ | `OPENAI_API_KEY` / `SHELLMATE_API_KEY` | `llm.api_key` |
76
+ | `SHELLMATE_BASE_URL` | `llm.base_url` |
77
+ | `SHELLMATE_MODEL` | `llm.model` |
78
+ | `SHELLMATE_SEARCH_ENDPOINT` | `search.endpoint` |
79
+
80
+ ## 架构
81
+
82
+ Agent 是一个 LangGraph 状态机:
83
+
84
+ ```mermaid
85
+ flowchart TD
86
+ Start([start]) --> SystemPrompt["system_prompt<br/>加载 Agent.md"]
87
+ SystemPrompt --> Assistant["assistant<br/>调用模型"]
88
+ Assistant -->|需要工具| Tools["tools<br/>网页搜索"]
89
+ Assistant -->|结束| End([end])
90
+ Tools --> Assistant
91
+ ```
92
+
93
+ - **system_prompt** — 加载可编辑的 `Agent.md` 作为系统提示词(每个会话加载一次)
94
+ - **assistant** — 携带消息历史调用 OpenAI 兼容模型
95
+ - **tools** — 模型请求联网时执行 DuckDuckGo 搜索
96
+
97
+ ## 项目结构
98
+
99
+ ```text
100
+ src/shellmate/
101
+ ├── agent.py # LangGraph agent + SQLite checkpoint
102
+ ├── cli.py # 命令行入口
103
+ ├── config.py # Pydantic 配置
104
+ ├── context.py # 历史格式化
105
+ ├── privacy.py # 脱敏中间件
106
+ ├── zsh_plugin.py # 内置 zsh 插件(从 shellmate.zsh 数据文件读取)
107
+ ├── shellmate.zsh # zsh 插件(Ctrl-G / preexec / precmd)
108
+ └── tools/
109
+ └── web_search.py # DuckDuckGo HTML 搜索
110
+ ```
@@ -0,0 +1,97 @@
1
+ # Shellmate
2
+
3
+ 🇨🇳 简体中文 | [🇬🇧 English](README.en.md)
4
+
5
+ Shellmate 是一个面向 zsh 的命令行 AI 助手。在命令行输入问题后按 **Ctrl-G**,它会结合近期命令历史,用任意 OpenAI 兼容模型(OpenAI、DeepSeek、Qwen 等)给出回答。
6
+
7
+ ## 功能
8
+
9
+ - **Ctrl-G** 快捷键 — 输入问题按 Ctrl-G 提问;**空缓冲按 Ctrl-G 自动解释上一条命令**
10
+ - 自动捕获上一条命令及其退出码,失败时结合退出码定位原因
11
+ - 近期命令历史作为上下文
12
+ - OpenAI 兼容协议,支持 OpenAI / DeepSeek / Qwen 等
13
+ - 内置 DuckDuckGo 网页搜索,无需搜索 API Key
14
+ - 模型与搜索请求前自动脱敏(含高熵密钥检测)
15
+ - 本地 SQLite 保存 checkpoint,无需数据库服务
16
+
17
+ ## 安装
18
+
19
+ 需要 Python 3.11+。
20
+
21
+ ```sh
22
+ pip install shellmate # 或从源码:pip install -e .
23
+ shellmate init # 创建配置 + zsh 插件 + .zshrc 加载行
24
+ source ~/.zshrc # 或重开终端
25
+ ```
26
+
27
+ ## 使用
28
+
29
+ 在 zsh 中输入问题,然后按 **Ctrl-G**。**空缓冲按 Ctrl-G**(命令行没有输入内容)会自动结合上一条命令及其退出码,解释它为什么失败。
30
+
31
+ ```sh
32
+ shellmate ask "刚才的命令为什么失败?" # 直接提问
33
+ shellmate ask # 交互式提问
34
+ shellmate ask --history $'ls -la\ngit status' "..." # 手动传历史
35
+
36
+ # 把命令输出喂给 Shellmate 解释(管道模式)
37
+ git push origin main 2>&1 | shellmate explain
38
+ tail -200 app.log | shellmate explain "为什么一直报 timeout?"
39
+
40
+ shellmate explain-last # 解释上一条命令(Ctrl-G 空缓冲触发)
41
+ shellmate config-path # 查看配置路径
42
+ shellmate history-lines # 查看历史条数
43
+ ```
44
+
45
+ ## 配置
46
+
47
+ `shellmate init` 会创建 `~/.config/shellmate/config.json` 和 `Agent.md`。在 config.json 中填入 API Key,或用环境变量设置。
48
+
49
+ ```json
50
+ {
51
+ "llm": { "base_url": "https://api.openai.com/v1", "model": "gpt-4o-mini", "api_key": "" },
52
+ "shell": { "history_lines": 20 },
53
+ "search": { "endpoint": "https://html.duckduckgo.com/html/" },
54
+ "privacy": { "redact_secrets": true, "redact_high_entropy": true, "custom_patterns": [] }
55
+ }
56
+ ```
57
+
58
+ 环境变量会覆盖 JSON 配置:
59
+
60
+ | 变量 | 覆盖 |
61
+ | --- | --- |
62
+ | `OPENAI_API_KEY` / `SHELLMATE_API_KEY` | `llm.api_key` |
63
+ | `SHELLMATE_BASE_URL` | `llm.base_url` |
64
+ | `SHELLMATE_MODEL` | `llm.model` |
65
+ | `SHELLMATE_SEARCH_ENDPOINT` | `search.endpoint` |
66
+
67
+ ## 架构
68
+
69
+ Agent 是一个 LangGraph 状态机:
70
+
71
+ ```mermaid
72
+ flowchart TD
73
+ Start([start]) --> SystemPrompt["system_prompt<br/>加载 Agent.md"]
74
+ SystemPrompt --> Assistant["assistant<br/>调用模型"]
75
+ Assistant -->|需要工具| Tools["tools<br/>网页搜索"]
76
+ Assistant -->|结束| End([end])
77
+ Tools --> Assistant
78
+ ```
79
+
80
+ - **system_prompt** — 加载可编辑的 `Agent.md` 作为系统提示词(每个会话加载一次)
81
+ - **assistant** — 携带消息历史调用 OpenAI 兼容模型
82
+ - **tools** — 模型请求联网时执行 DuckDuckGo 搜索
83
+
84
+ ## 项目结构
85
+
86
+ ```text
87
+ src/shellmate/
88
+ ├── agent.py # LangGraph agent + SQLite checkpoint
89
+ ├── cli.py # 命令行入口
90
+ ├── config.py # Pydantic 配置
91
+ ├── context.py # 历史格式化
92
+ ├── privacy.py # 脱敏中间件
93
+ ├── zsh_plugin.py # 内置 zsh 插件(从 shellmate.zsh 数据文件读取)
94
+ ├── shellmate.zsh # zsh 插件(Ctrl-G / preexec / precmd)
95
+ └── tools/
96
+ └── web_search.py # DuckDuckGo HTML 搜索
97
+ ```
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "shellmate-ai"
3
+ version = "0.1.0"
4
+ description = "An AI assistant for your zsh command line"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "langgraph>=1.0,<2.0",
9
+ "langgraph-checkpoint-sqlite>=3.0,<4.0",
10
+ "langchain-openai>=1.0,<2.0",
11
+ "pydantic>=2.7,<3.0",
12
+ ]
13
+
14
+ [project.scripts]
15
+ shellmate = "shellmate.cli:main"
16
+
17
+ [build-system]
18
+ requires = ["setuptools>=68"]
19
+ build-backend = "setuptools.build_meta"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+
24
+ [tool.setuptools.package-data]
25
+ shellmate = ["*.zsh"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Shellmate:面向 shell 的 AI 助手。"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,142 @@
1
+ """基于 LangGraph 构建带工具调用和本地 SQLite 会话记忆的 Agent。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, TypedDict
6
+
7
+ from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage, ToolMessage
8
+ from langchain_core.tools import tool
9
+ from langchain_openai import ChatOpenAI
10
+ from langgraph.checkpoint.sqlite import SqliteSaver
11
+ from langgraph.graph import END, START, StateGraph
12
+ from langgraph.graph.message import add_messages
13
+ from langgraph.prebuilt import ToolNode, tools_condition
14
+
15
+ from shellmate.config import AGENT_PROMPT_PATH, CHECKPOINT_DB_PATH, AppConfig, ensure_data_dir
16
+ from shellmate.context import ShellContext
17
+ from shellmate.privacy import SecretRedactionMiddleware
18
+ from shellmate.tools.web_search import web_search
19
+
20
+ def load_system_prompt() -> str:
21
+ """读取用户配置目录的 Agent.md,作为每次模型调用的系统提示词。"""
22
+ try:
23
+ prompt = AGENT_PROMPT_PATH.read_text(encoding="utf-8").strip()
24
+ except OSError as exc:
25
+ raise AgentError(f"无法读取系统提示词文件 {AGENT_PROMPT_PATH}:{exc}") from exc
26
+ if not prompt:
27
+ raise AgentError(f"系统提示词文件不能为空:{AGENT_PROMPT_PATH}")
28
+ return prompt
29
+
30
+
31
+ class AgentState(TypedDict):
32
+ """图内共享状态;add_messages 负责按 ID 合并并追加消息。"""
33
+
34
+ messages: Annotated[list[AnyMessage], add_messages]
35
+
36
+
37
+ class AgentError(RuntimeError):
38
+ """Agent 配置、图运行或持久化失败时抛出的异常。"""
39
+
40
+
41
+ class LangGraphAgent:
42
+ """构建模型与工具节点,并按 thread_id 从本地 SQLite 恢复会话。"""
43
+
44
+ def __init__(self, config: AppConfig):
45
+ self.config = config
46
+ ensure_data_dir()
47
+ self.privacy = SecretRedactionMiddleware(
48
+ config.privacy.redact_secrets,
49
+ config.privacy.custom_patterns,
50
+ config.privacy.redact_high_entropy,
51
+ )
52
+
53
+ @tool
54
+ def search_web(query: str) -> str:
55
+ """使用 DuckDuckGo 网页搜索查询最新信息或软件文档。"""
56
+ return web_search(query, config.search.endpoint)
57
+
58
+ self.tools = [search_web]
59
+ if not config.llm.api_key:
60
+ raise AgentError("请在 llm.api_key 中配置密钥,或设置 OPENAI_API_KEY / SHELLMATE_API_KEY。")
61
+ # 所有服务都通过 OpenAI 兼容协议接入;差异只体现在地址、模型和密钥。
62
+ self.model = ChatOpenAI(
63
+ model=config.llm.model,
64
+ api_key=config.llm.api_key,
65
+ base_url=config.llm.base_url,
66
+ timeout=config.llm.timeout,
67
+ ).bind_tools(self.tools)
68
+
69
+ def _load_system_prompt(self, state: AgentState) -> dict[str, list[AnyMessage]]:
70
+ """系统提示词节点:读取 Agent.md 并作为首条消息加入图状态。
71
+
72
+ 每个 thread 只注入一次;同一会话后续轮次的状态里已有系统提示词时跳过,
73
+ 避免重复追加。用户编辑 Agent.md 后,新会话(新 thread)会读取最新内容。
74
+ """
75
+ if any(isinstance(message, SystemMessage) for message in state["messages"]):
76
+ return {}
77
+ return {"messages": [SystemMessage(content=load_system_prompt())]}
78
+
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)
83
+ return {"messages": [response]}
84
+
85
+ def _wrap_tool_call(self, request, execute):
86
+ """LangGraph 中间件:在工具执行前后对输入与输出脱敏。
87
+
88
+ 工具输入(如搜索 query)可能含未识别出的秘密,先脱敏再外发到搜索
89
+ 服务,避免泄漏给第三方;工具输出则先脱敏再写回状态,避免敏感内容
90
+ 被本地 checkpoint 持久化或再次送入模型。
91
+ """
92
+ call = request.tool_call
93
+ args = call.get("args")
94
+ if isinstance(args, dict):
95
+ safe_args = {
96
+ key: self.privacy.before_tool(value) if isinstance(value, str) else value
97
+ for key, value in args.items()
98
+ }
99
+ request = request.override(tool_call={**call, "args": safe_args})
100
+ result = execute(request)
101
+ if isinstance(result, ToolMessage) and isinstance(result.content, str):
102
+ result = result.model_copy(update={"content": self.privacy.before_tool(result.content)})
103
+ return result
104
+
105
+ def _build_graph(self, checkpointer: SqliteSaver):
106
+ """连接系统提示词、模型和工具节点,并注入本地 SQLite checkpoint。"""
107
+ graph = StateGraph(AgentState)
108
+ graph.add_node("system_prompt", self._load_system_prompt)
109
+ graph.add_node("assistant", self._call_model)
110
+ graph.add_node("tools", ToolNode(self.tools, wrap_tool_call=self._wrap_tool_call))
111
+ graph.add_edge(START, "system_prompt")
112
+ graph.add_edge("system_prompt", "assistant")
113
+ graph.add_conditional_edges("assistant", tools_condition, {"tools": "tools", END: END})
114
+ graph.add_edge("tools", "assistant")
115
+ return graph.compile(checkpointer=checkpointer)
116
+
117
+ def ask(self, question: str, context: ShellContext, thread_id: str) -> str:
118
+ """用 LangGraph 配置中的 thread_id 恢复并更新本地 shell 会话。"""
119
+ thread_id = thread_id.strip()
120
+ if not thread_id or len(thread_id) > 128:
121
+ raise AgentError("thread_id 必须为 1 到 128 个字符。")
122
+ graph_config = {"configurable": {"thread_id": thread_id}}
123
+ user_content = f"{context.as_text()}\n\nQuestion: {question}"
124
+ # 脱敏后再写入 LangGraph 状态,避免原始敏感值被 checkpoint 持久化。
125
+ user_content = self.privacy.redact(user_content)
126
+ try:
127
+ # SqliteSaver 会在本地文件中保存同一 thread_id 的跨进程状态。
128
+ with SqliteSaver.from_conn_string(str(CHECKPOINT_DB_PATH)) as checkpointer:
129
+ checkpointer.setup()
130
+ graph = self._build_graph(checkpointer)
131
+ result = graph.invoke(
132
+ {"messages": [HumanMessage(content=user_content)]},
133
+ config=graph_config,
134
+ )
135
+ except Exception as exc:
136
+ raise AgentError(f"LangGraph 执行或 SQLite 持久化失败:{exc}") from exc
137
+
138
+ last_message = result["messages"][-1]
139
+ content = last_message.content
140
+ if isinstance(content, str):
141
+ return content or "(No response content.)"
142
+ return "\n".join(str(block.get("text", block)) for block in content)
@@ -0,0 +1,159 @@
1
+ """Shellmate 命令行入口,负责解析参数并调用相应模块。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+
9
+ from shellmate.agent import AgentError, LangGraphAgent
10
+ from shellmate.config import (
11
+ AGENT_PROMPT_PATH,
12
+ CONFIG_PATH,
13
+ DATA_DIR,
14
+ ZSH_PLUGIN_PATH,
15
+ ensure_data_dir,
16
+ ensure_zsh_plugin,
17
+ load_config,
18
+ )
19
+ from shellmate.context import ShellContext, read_context, read_zsh_history
20
+
21
+ # 管道模式输入的最大保留字符数;报错信息通常在输出末尾,故保留尾部。
22
+ _MAX_OUTPUT_CHARS = 20000
23
+
24
+
25
+ def _history_text(config, args) -> str:
26
+ """历史来源优先级:zsh 插件经 --history 传入 > 环境变量 > 历史文件兜底。"""
27
+ return (
28
+ getattr(args, "history", "")
29
+ or os.environ.get("SHELLMATE_HISTORY_TEXT", "")
30
+ or read_zsh_history(config.shell.history_lines)
31
+ )
32
+
33
+
34
+ def _thread_id(config, args) -> str:
35
+ """会话 ID 来源优先级:--thread-id > zsh 会话环境变量 > 配置兜底。"""
36
+ return (
37
+ getattr(args, "thread_id", None)
38
+ or os.environ.get("SHELLMATE_SESSION_ID")
39
+ or config.thread_id
40
+ )
41
+
42
+
43
+ def _ask(config, question: str, context: ShellContext, args) -> None:
44
+ """运行 agent 并打印回答,统一异常处理。"""
45
+ try:
46
+ print(LangGraphAgent(config).ask(question, context, _thread_id(config, args)))
47
+ except (AgentError, ValueError, OSError) as exc:
48
+ print(f"shellmate: {exc}", file=sys.stderr)
49
+ raise SystemExit(1) from exc
50
+
51
+
52
+ def main() -> None:
53
+ """处理初始化、提问、解释上一条命令与配置查看等子命令。"""
54
+ parser = argparse.ArgumentParser(prog="shellmate", description="Ask an AI assistant about your shell session")
55
+ sub = parser.add_subparsers(dest="command")
56
+ ask_parser = sub.add_parser("ask", help="Ask a question using recent shell context")
57
+ ask_parser.add_argument("question", nargs="*", help="Question; multiple words are joined with spaces")
58
+ ask_parser.add_argument("--history", default="", help="Recent shell history supplied by the zsh plugin")
59
+ ask_parser.add_argument("--thread-id", help="LangGraph conversation ID; normally supplied by zsh")
60
+ explain_parser = sub.add_parser(
61
+ "explain",
62
+ help="Explain command output read from stdin (e.g. cmd 2>&1 | shellmate explain)",
63
+ )
64
+ explain_parser.add_argument("question", nargs="*", help="Optional question; defaults to summarizing the output")
65
+ explain_parser.add_argument("--history", default="", help="Recent shell history supplied by the zsh plugin")
66
+ explain_parser.add_argument("--thread-id", help="LangGraph conversation ID; normally supplied by zsh")
67
+ last_parser = sub.add_parser(
68
+ "explain-last",
69
+ help="Explain the last command and its exit code (triggered by Ctrl-G on empty prompt)",
70
+ )
71
+ last_parser.add_argument("--history", default="", help="Recent shell history supplied by the zsh plugin")
72
+ last_parser.add_argument("--thread-id", help="LangGraph conversation ID; normally supplied by zsh")
73
+ sub.add_parser("init", help="Create the local configuration and Agent prompt files")
74
+ sub.add_parser("config-path", help="Print the configuration file path")
75
+ sub.add_parser("history-lines", help="Print the configured number of history lines")
76
+ args = parser.parse_args()
77
+ try:
78
+ config = load_config()
79
+ except (ValueError, OSError) as exc:
80
+ print(f"shellmate: {exc}", file=sys.stderr)
81
+ raise SystemExit(2) from exc
82
+
83
+ if args.command == "init":
84
+ ensure_data_dir()
85
+ zshrc_updated = ensure_zsh_plugin()
86
+ print(f"已创建或确认配置文件:{CONFIG_PATH}")
87
+ print(f"已创建或确认 Agent 提示词:{AGENT_PROMPT_PATH}")
88
+ print(f"已创建或确认数据目录:{DATA_DIR}")
89
+ print(f"已创建或确认 zsh 插件:{ZSH_PLUGIN_PATH}")
90
+ if zshrc_updated:
91
+ print("已在 ~/.zshrc 添加插件加载行,请执行 `source ~/.zshrc` 使其生效。")
92
+ else:
93
+ print("~/.zshrc 已包含插件加载行,无需修改。")
94
+ return
95
+ if args.command == "config-path":
96
+ print(CONFIG_PATH)
97
+ return
98
+ if args.command == "history-lines":
99
+ print(config.shell.history_lines)
100
+ return
101
+
102
+ if args.command == "explain-last":
103
+ last_command = os.environ.get("SHELLMATE_LAST_COMMAND", "").strip()
104
+ if not last_command:
105
+ print("shellmate: 没有可解释的上一条命令。", file=sys.stderr)
106
+ raise SystemExit(1)
107
+ raw_exit = os.environ.get("SHELLMATE_LAST_EXIT", "").strip()
108
+ try:
109
+ exit_code = int(raw_exit)
110
+ except ValueError:
111
+ exit_code = None
112
+ history = _history_text(config, args)
113
+ context = ShellContext(history=history, last_command=last_command, last_exit_code=exit_code)
114
+ if exit_code == 0:
115
+ question = "解释一下刚才这条命令:它做了什么、输出或副作用是什么、有什么值得注意的地方。"
116
+ else:
117
+ question = "刚才这条命令失败了。结合退出码解释它为什么会失败,并给出如何排查和修复的具体建议。"
118
+ _ask(config, question, context, args)
119
+ return
120
+
121
+ if args.command == "explain":
122
+ if sys.stdin.isatty():
123
+ print(
124
+ "shellmate: explain 需要管道输入,例如:command 2>&1 | shellmate explain",
125
+ file=sys.stderr,
126
+ )
127
+ raise SystemExit(1)
128
+ output = sys.stdin.read()
129
+ if not output.strip():
130
+ print("shellmate: 标准输入为空。", file=sys.stderr)
131
+ raise SystemExit(1)
132
+ output = output[-_MAX_OUTPUT_CHARS:]
133
+ question = " ".join(args.question).strip() or "解释这段命令输出:发生了什么、是否报错、以及该如何处理。"
134
+ history = _history_text(config, args)
135
+ context = ShellContext(history=history, output=output)
136
+ _ask(config, question, context, args)
137
+ return
138
+
139
+ if args.command != "ask":
140
+ parser.print_help()
141
+ return
142
+
143
+ if args.question:
144
+ question = " ".join(args.question).strip()
145
+ else:
146
+ try:
147
+ question = input("Ask Shellmate: ").strip()
148
+ except (EOFError, KeyboardInterrupt):
149
+ print("\nshellmate: 未输入问题。", file=sys.stderr)
150
+ raise SystemExit(1)
151
+ if not question:
152
+ print("shellmate: 未输入问题。", file=sys.stderr)
153
+ raise SystemExit(1)
154
+ context = read_context(_history_text(config, args))
155
+ _ask(config, question, context, args)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()