prompt-copilot-cli 0.2.2__tar.gz → 0.2.6__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 (21) hide show
  1. {prompt_copilot_cli-0.2.2/prompt_copilot_cli.egg-info → prompt_copilot_cli-0.2.6}/PKG-INFO +1 -1
  2. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/main.py +278 -139
  3. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6/prompt_copilot_cli.egg-info}/PKG-INFO +1 -1
  4. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/pyproject.toml +1 -1
  5. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/tests/test_cli_args.py +37 -0
  6. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/LICENSE +0 -0
  7. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/MANIFEST.in +0 -0
  8. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/README.md +0 -0
  9. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/README.zh-CN.md +0 -0
  10. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/prompt_copilot_cli.egg-info/SOURCES.txt +0 -0
  11. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/prompt_copilot_cli.egg-info/dependency_links.txt +0 -0
  12. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/prompt_copilot_cli.egg-info/entry_points.txt +0 -0
  13. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/prompt_copilot_cli.egg-info/requires.txt +0 -0
  14. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/prompt_copilot_cli.egg-info/top_level.txt +0 -0
  15. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/requirements.txt +0 -0
  16. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/setup.cfg +0 -0
  17. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/tests/test_command_timeout.py +0 -0
  18. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/tests/test_file_tools.py +0 -0
  19. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/tests/test_image_tool.py +0 -0
  20. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/tests/test_mcp_transport.py +0 -0
  21. {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.6}/tests/test_multimodal_messages.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompt-copilot-cli
3
- Version: 0.2.2
3
+ Version: 0.2.6
4
4
  Summary: A lightweight terminal-based coding agent with file tools, command execution, multimodal image support, and MCP integration.
5
5
  Author: Jason Li
6
6
  License-Expression: MIT
@@ -94,10 +94,9 @@ def build_bottom_toolbar_text() -> str:
94
94
  return f"{token_summary} | {base}"
95
95
 
96
96
 
97
- ZH_SYSTEM_PROMPT = """
98
- 你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,专注于使用文件、命令、Python 脚本等工具完成开发任务。
99
97
 
100
- 工作原则:
98
+
99
+ DEFAULT_SYSTEM_WORKING_PRINCIPLES_ZH = """工作原则:
101
100
  1、先检查工作目录并理解需求。
102
101
  2、优先使用网络搜索类工具搜索网络实时信息,例如:天气、新闻、资讯等等。
103
102
  3、所有代码文件生成、编辑、删除等操作均在工作目录中执行。如果用户没有指定具体目录,默认工作目录为工程的根目录。
@@ -108,16 +107,13 @@ ZH_SYSTEM_PROMPT = """
108
107
  8、如果用户想“读取/识别其他二进制文件”,你必须明确告知:我无法直接读取或理解通用二进制文件内容。
109
108
  9、如果用户想“查看图片内容”,应优先调用图片读取工具将图片转成 base64 或视觉消息格式,再将其发送给支持多模态的模型。
110
109
  10、对于图片类任务,优先使用图片工具而不是尝试直接读取二进制原始内容。
111
- 11、当用户输入一条任务指令时,如果是详细的任务清单,你就理解用户要求逐步完成工作,同时每个步骤执行完毕要反馈状态和进度。
112
- 12、在执行命令前,先判断该命令是短时命令还是持久命令:短时命令如 ls、pytest、python -m compileall 等应同步执行并等待结果;持久命令如 npm run dev、python app.py、uvicorn、flask run、serve、http.server 等会持续运行,应该优先以后台方式启动,并在必要时用健康检查确认服务已就绪。
110
+ 11、当用户输入一条任务指令时,如果是详细的任务清单,你就理解用户要求逐步完成工作,同时执行每个步骤的时候明确说明当前环节与进度,实时反馈任务状态。
111
+ 12、使用 execute_python_script 时必须在脚本内自行管理进程生命周期:若是 ls、pytest、build 等短命令用 subprocess.run 显式设置 timeout,若是 npm run dev、flask run 等常驻服务用 subprocess.Popen 脱离终端启动并在脚本内轮询健康检查 URL,确认就绪后打印 PID 并立即 sys.exit() 退出以防触发 360 秒强制超时。
113
112
  13、无法调用官方联网检索工具而通过脚本抓取网络内容时需区分数据类型处理,抓取普通网页文本需剔除 HTML 标签、样式代码、广告碎片、无效注释、多余空行等冗余内容,仅留存有效正文且文本超出 8000 字符则截断,文本输出上限为 8000 字符,抓取程序源代码则无需过滤,完整保留原始代码、自带注释、缩进换行与原有格式,代码输出无字符数量限制。
113
+ 14、脱离终端启动常驻服务时务必兼顾跨平台兼容:Linux/Mac 环境设置 start_new_session=True,Windows 环境设置 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP。
114
+ 15、凡耗时不可控的超长任务(如大文件下载、模型训练、全量测试、复杂构建等),严禁在脚本内同步阻塞,必须用 subprocess.Popen 跨平台脱离终端启动并将进度心跳写入本地日志文件,打印 PID 后立即 sys.exit(),工具 timeout_seconds 设为 15 秒仅验证启动;后续须通过检查日志监控进度,若发现任务完成或长时间无心跳更新,必须主动调用系统命令(如 kill -9 PID)清理后台进程以防孤儿进程耗尽资源;仅在任务自身支持超时参数或耗时可预估时,方可在脚本内同步执行并设合理 timeout_seconds。"""
114
115
 
115
- """
116
-
117
- EN_SYSTEM_PROMPT = """
118
- You are a CLI-based coding agent named Jason Li, focused on completing development tasks by using files, commands, Python scripts, and other tools.
119
-
120
- Working principles:
116
+ DEFAULT_SYSTEM_WORKING_PRINCIPLES_EN = """Working principles:
121
117
  1. First inspect the working directory and understand the requirement.
122
118
  2. Prefer using network search tools for real-time information, such as weather, news, or other current topics.
123
119
  3. All code file creation, editing, and deletion operations are performed in the working directory. If the user does not specify a directory, the project root is used by default.
@@ -128,37 +124,71 @@ Working principles:
128
124
  8. If the user wants to read or recognize other binary files, you must clearly state that you cannot directly read or understand generic binary file contents.
129
125
  9. If the user wants to inspect image content, you should first use the image-reading tool to convert the image into base64 or a visual-message format and then send it to a multimodal-capable model.
130
126
  10. For image-related tasks, prefer the image tool rather than trying to read the raw binary contents directly.
131
- 11. When the user gives a task instruction, first understand the full intent from the context and the conversation history, then generate a clear execution checklist. After that, follow the checklist step by step to complete the task and report the current status and progress after each step.
132
- 12. Before executing a command, first determine whether it is a short-lived command or a persistent one: short-lived commands such as ls, pytest, and python -m compileall should be executed synchronously and awaited; persistent commands such as npm run dev, python app.py, uvicorn, flask run, serve, and http.server will keep running, so they should be started in the background and, when needed, checked for readiness with a health check.
133
- 13、When official network retrieval tools are unavailable and network content is crawled via scripts, differentiated processing shall be performed according to data types: for regular webpage texts, remove redundant contents including HTML tags, style codes, advertising fragments, invalid comments and redundant blank lines, retain only valid main texts and truncate the content if it exceeds 8000 characters with a strict upper limit of 8000 characters for text output; for program source codes, no filtering or cleaning is required, keep the original codes, built-in comments, indentations, line breaks and original formats completely with no character limit on code output.
127
+ 11. When the user inputs a task instruction, if it is a detailed task list, understand the user's requirements and execute the tasks step by step, providing real-time feedback on the current status and progress.
128
+ 12. When using the execute_python_script tool, manage the process lifecycle within the script: for short commands like ls, pytest, and build, use subprocess.run with an explicit timeout; for persistent commands like npm run dev and flask run, use subprocess.Popen to start them in the background and poll a health check URL to confirm they are ready before printing the PID and exiting immediately to avoid triggering the 360-second timeout.
129
+ 13. When official network retrieval tools are unavailable and network content is crawled via scripts, differentiated processing shall be performed according to data types: for regular webpage texts, remove redundant contents including HTML tags, style codes, advertising fragments, invalid comments and redundant blank lines, retain only valid main texts and truncate the content if it exceeds 8000 characters with a strict upper limit of 8000 characters for text output; for program source codes, no filtering or cleaning is required, keep the original codes, built-in comments, indentations, line breaks and original formats completely with no character limit on code output.
130
+ 14. When starting a persistent service in the background, ensure cross-platform compatibility: set start_new_session=True for Linux/Mac environments and creationflags=subprocess.CREATE_NEW_PROCESS_GROUP for Windows environments.
131
+ 15. For long-running tasks with unpredictable durations (such as large file downloads, model training, full tests, complex builds, etc.), synchronous blocking execution within the script is strictly prohibited. Instead, use subprocess.Popen to start the task in the background across platforms and write progress heartbeats to a local log file. After printing the PID, immediately call sys.exit() and set the tool's timeout_seconds to 15 seconds for startup verification only. Subsequently, monitor progress by checking the log file. If the task completes or there is no heartbeat update for an extended period, proactively call system commands (like kill -9 PID) to clean up background processes and prevent orphan processes from consuming resources. Only when the task itself supports timeout parameters or has a predictable duration can synchronous execution be performed within the script with a reasonable timeout_seconds setting."""
132
+
133
+ TASK_END_GUIDANCE_ZH = """你是提示词优化专家,负责将对话记录整理为更清晰、可执行的最终任务提示词。
134
+
135
+ 请与默认系统提示词保持一致的工作原则与约束条件:
136
+ {principles}
137
+
138
+ 生成最终提示词时,请严格遵循以下约束:
139
+ 1. 只输出最终改进后的提示词文本,不要添加任何解释、元信息或注释。
140
+ 2. 保留用户原始目标、关键上下文、澄清或变更后的要求、待执行步骤与预期产物。
141
+ 3. 语言必须清晰、可执行、步骤化,适合直接给后续执行器使用。
142
+ 4. 如果上下文中存在不确定信息,应显式写成“待确认”或“需验证”的约束项。
143
+ 5. 输出长度控制在 18000 字符以内。"""
144
+
145
+ TASK_END_GUIDANCE_EN = """You are a prompt-optimization expert responsible for turning conversation history into a clearer and more actionable final task prompt.
146
+
147
+ Please follow the same working principles and constraints as the default system prompt:
148
+ {principles}
149
+
150
+ When generating the final prompt, strictly follow these constraints:
151
+ 1. Return only the improved final prompt text. Do not add explanations, metadata, or comments.
152
+ 2. Preserve the user's original goal, key context, clarified or changed requirements, pending steps, and expected deliverables.
153
+ 3. Use clear, executable, step-by-step language suitable for direct use by a downstream executor.
154
+ 4. If the context contains uncertain information, make it explicit as a "to confirm" or "to verify" constraint.
155
+ 5. Keep the output within 18000 characters."""
156
+
157
+ ZH_SYSTEM_PROMPT = f"""
158
+ 你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,专注于使用文件、命令、Python 脚本等工具完成开发任务。
159
+
160
+ {DEFAULT_SYSTEM_WORKING_PRINCIPLES_ZH}
134
161
 
135
162
  """
136
163
 
164
+ EN_SYSTEM_PROMPT = f"""
165
+ You are a CLI-based coding agent named Jason Li, focused on completing development tasks by using files, commands, Python scripts, and other tools.
166
+
167
+ {DEFAULT_SYSTEM_WORKING_PRINCIPLES_EN}
137
168
 
138
- PLANNING_PROMPT = (
139
- "你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,专注于使用文件、命令、Python 脚本等工具完成开发任务。\n\n"
140
- "在生成任务执行计划时,请遵循以下工作原则:\n"
141
- "1、先检查工作目录并理解需求。\n"
142
- "2、优先使用网络搜索类工具搜索网络实时信息,例如:天气、新闻、资讯等等。\n"
143
- "3、所有代码文件生成、编辑、删除等操作均在工作目录中执行。如果用户没有指定具体目录,默认工作目录为工程的根目录。\n"
144
- "4、如果需要,输出简洁的说明与下一步建议。\n"
145
- "5、若涉及删除系统文件或执行危险命令,必须先向用户确认后方可执行。\n"
146
- "6、当用户输入指令:/task-start 的时候,直接回复:请输入你的第一条初始提示。\n"
147
- "7、遇到信息盲区时,严禁主观臆测,请优先使用搜索工具补齐信息缺口,确保回答精准有据。\n"
148
- "8、如果用户想“读取/识别其他二进制文件”,你必须明确告知:我无法直接读取或理解通用二进制文件内容。\n"
149
- "9、如果用户想“查看图片内容”,应优先调用图片读取工具将图片转成 base64 或视觉消息格式,再将其发送给支持多模态的模型。\n"
150
- "10、对于图片类任务,优先使用图片工具而不是尝试直接读取二进制原始内容。\n"
151
- "11、基于上下文和历史对话,先梳理用户的完整意图,再生成一份清晰、可执行的步骤清单;随后按步骤逐步完成任务,并在每个步骤完成后反馈执行状态和进度。\n"
152
- "12、在任何需要执行命令的步骤中,先判断该命令属于短时命令还是持久命令,并在任务清单中明确写出你的判断说明:短时命令如 ls、pytest、python -m compileall 等应同步执行并等待结果;持久命令如 npm run dev、python app.py、uvicorn、flask run、serve、http.server 等应优先以后台方式启动,并在必要时用健康检查确认服务已就绪。\n\n"
153
- "13、无法调用官方联网检索工具而通过脚本抓取网络内容时需区分数据类型处理,抓取普通网页文本需剔除 HTML 标签、样式代码、广告碎片、无效注释、多余空行等冗余内容,仅留存有效正文且文本超出 8000 字符则截断,文本输出上限为 8000 字符,抓取程序源代码则无需过滤,完整保留原始代码、自带注释、缩进换行与原有格式,代码输出无字符数量限制。\n\n"
154
- "请严格按照以下格式输出结果:\n\n"
155
- "用户原始指令:......\n\n"
156
- "结合上下文得到用户的完整意图:.....\n\n"
157
- "接下来按照这个步骤逐步执行完成任务:\n\n"
158
- "1、第一步:......\n"
159
- "2、第二步:......\n"
160
- "......"
161
- )
169
+ """
170
+
171
+
172
+ PLANNING_PROMPT = f"""
173
+ 你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,能够通过使用文件、命令、Python 脚本等工具完成开发任务,当前你只能够生成任务执行计划。
174
+
175
+ 注意:你的输出只能是“任务执行计划”的文本说明。绝对不能真实执行任何命令、脚本、文件修改或调用任何工具方法。你是在“规划”而非“动手”。
176
+
177
+ 在生成任务执行计划时,请遵循以下{DEFAULT_SYSTEM_WORKING_PRINCIPLES_ZH}
178
+
179
+
180
+ 请严格按照以下格式输出结果:
181
+
182
+ 用户原始指令:......
183
+
184
+ 结合上下文得到用户的完整意图:.....
185
+
186
+ 接下来按照这个步骤逐步执行完成任务:
187
+ 1、第一步:......
188
+ 2、第二步:......
189
+ ......
190
+
191
+ """
162
192
 
163
193
 
164
194
  TRANSLATIONS = {
@@ -166,6 +196,10 @@ TRANSLATIONS = {
166
196
  "zh": ZH_SYSTEM_PROMPT,
167
197
  "en": EN_SYSTEM_PROMPT,
168
198
  },
199
+ "task_end_system_prompt": {
200
+ "zh": TASK_END_GUIDANCE_ZH.format(principles=DEFAULT_SYSTEM_WORKING_PRINCIPLES_ZH),
201
+ "en": TASK_END_GUIDANCE_EN.format(principles=DEFAULT_SYSTEM_WORKING_PRINCIPLES_EN),
202
+ },
169
203
  "config_field_model": {"zh": "模型名称,例如 qwen2.5-7b-instruct", "en": "Model name, for example qwen2.5-7b-instruct"},
170
204
  "config_field_base_url": {"zh": "OpenAI 兼容接口地址,例如 http://127.0.0.1:11434/v1", "en": "OpenAI-compatible base URL, for example http://127.0.0.1:11434/v1"},
171
205
  "config_field_api_key": {"zh": "API Key;若是本地模型可填写任意非空字符串", "en": "API key; for a local model you can enter any non-empty string"},
@@ -273,7 +307,6 @@ TRANSLATIONS = {
273
307
  "context_size_label": {"zh": "提交上下文大小", "en": "Submitted context size"},
274
308
  "response_length_label": {"zh": "回复内容长度", "en": "Response content length"},
275
309
  "recent_conversations_header": {"zh": "# 最近对话记录", "en": "# Recent conversations"},
276
- "task_end_system_prompt": {"zh": "你是提示词优化专家(精通将对话记录转化为清晰、可执行的任务提示)。\n输入:首先是用户在输入 /task-start 后会给出第一条初始提示,随后是从该标记以来的对话记录(包括模型回复、工具调用与结果)。\n任务:阅读这些记录,理解用户起始提示与后续的澄清或更改,并结合这些信息生成一个改进后的、清晰且可直接用于执行的最终提示词。\n输出要求:只输出最终改进后的提示词文本,不要添加任何解释、元信息或注释。长度控制在 18000 字符以内。", "en": "You are a prompt-optimization expert skilled at turning conversation history into a clear and actionable task prompt.\nInput: first the user provides an initial prompt after /task-start, then the conversation history from that point onward including model replies, tool calls, and results.\nTask: read these records, understand the initial prompt and any follow-up clarifications or changes, and generate an improved final prompt that is clear, executable, and ready to use.\nOutput requirements: return only the final improved prompt text, with no explanation, metadata, or comments. Keep it within 18000 characters."},
277
310
  "task_end_user_message": {"zh": "对话记录(从 /task-start 开始,按时间从旧到新):\n\n{compiled_text}", "en": "Conversation history (starting from /task-start, from oldest to newest):\n\n{compiled_text}"},
278
311
  "task_end_generation_failed_log": {"zh": "调用模型生成最终提示失败", "en": "Failed to generate the final prompt with the model"},
279
312
  "final_prompt_header": {"zh": "# 最终提示词", "en": "# Final prompt"},
@@ -289,7 +322,7 @@ def t(key: str, **kwargs: Any) -> str:
289
322
 
290
323
 
291
324
  UI_SYSTEM_LANGUAGE = "en"
292
- APPLICATION_VERSION = "0.2.2"
325
+ APPLICATION_VERSION = "0.2.6"
293
326
  DEFAULT_SYSTEM_PROMPT = t("system_prompt")
294
327
  WORKSPACE_DIR = ROOT / "workspace"
295
328
  LOG_DIR = ROOT / "logs"
@@ -475,38 +508,31 @@ TOOL_DEFINITIONS = [
475
508
  },
476
509
  },
477
510
  },
478
- {
479
- "type": "function",
480
- "function": {
481
- "name": "execute_command",
482
- "description": t("execute_command_desc"),
483
- "parameters": {
484
- "type": "object",
485
- "properties": {
486
- "command": {"type": "string", "description": t("command_desc")},
487
- "cwd": {"type": "string", "description": t("cwd_desc")},
488
- "timeout_seconds": {"type": "integer", "description": "Maximum runtime in seconds before the background process is terminated automatically."},
489
- "output_log_path": {"type": "string", "description": "Optional path to a log file where stdout/stderr will be written."},
490
- },
491
- "required": ["command", "cwd"],
492
- },
493
- },
494
- },
495
511
  {
496
512
  "type": "function",
497
513
  "function": {
498
514
  "name": "execute_python_script",
499
- "description": t("execute_python_script_desc"),
515
+ "description": "Execute a Python script or a block of Python code. Use the 'timeout_seconds' parameter to control the maximum execution time and prevent the process from hanging indefinitely.",
500
516
  "parameters": {
501
517
  "type": "object",
502
518
  "properties": {
503
- "script": {"type": "string", "description": t("script_desc")},
504
- "cwd": {"type": "string", "description": t("script_cwd_desc")},
519
+ "script": {
520
+ "type": "string",
521
+ "description": "The Python script content to execute."
522
+ },
523
+ "cwd": {
524
+ "type": "string",
525
+ "description": "The working directory for the script execution."
526
+ },
527
+ "timeout_seconds": {
528
+ "type": "integer",
529
+ "description": "Maximum execution time in seconds before the process is terminated. Defaults to 120. Set a shorter timeout for quick commands to avoid waiting."
530
+ }
505
531
  },
506
- "required": ["script", "cwd"],
507
- },
508
- },
509
- },
532
+ "required": ["script", "cwd"]
533
+ }
534
+ }
535
+ }
510
536
  ]
511
537
  ACTIVE_MCP_TOOL_DEFINITIONS: list[dict[str, Any]] = []
512
538
  ACTIVE_MCP_TOOL_CONFIG: dict[str, Any] = {}
@@ -1211,51 +1237,193 @@ def wait_for_health_check(url: str, timeout_seconds: int = 20) -> bool:
1211
1237
 
1212
1238
  def run_subprocess_command(command: Any, cwd: str, shell: bool = False, timeout: int | None = None) -> tuple[int, str, str]:
1213
1239
  safe_cwd = resolve_execution_cwd(cwd, Path.cwd())
1240
+
1241
+ # 跨平台进程隔离配置
1242
+ popen_kwargs = {
1243
+ "cwd": safe_cwd,
1244
+ "stdout": subprocess.PIPE,
1245
+ "stderr": subprocess.PIPE,
1246
+ "stdin": subprocess.DEVNULL, # 根治等待输入导致的卡死!
1247
+ "text": True,
1248
+ "encoding": "utf-8",
1249
+ "errors": "replace",
1250
+ }
1251
+ if platform.system() == "Windows":
1252
+ popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
1253
+ else:
1254
+ popen_kwargs["start_new_session"] = True # 创建新进程组,便于后续杀整棵树
1255
+
1214
1256
  try:
1215
- proc = subprocess.Popen(
1216
- command,
1217
- cwd=safe_cwd,
1218
- shell=shell,
1219
- stdout=subprocess.PIPE,
1220
- stderr=subprocess.PIPE,
1221
- text=True,
1222
- encoding="utf-8",
1223
- errors="replace",
1224
- )
1257
+ proc = subprocess.Popen(command, shell=shell, **popen_kwargs)
1225
1258
  except (FileNotFoundError, NotADirectoryError, OSError) as exc:
1226
1259
  logger.warning(t("tool_subprocess_failed"), cwd, safe_cwd, exc)
1227
- proc = subprocess.Popen(
1228
- command,
1229
- cwd=str(Path.cwd()),
1230
- shell=shell,
1231
- stdout=subprocess.PIPE,
1232
- stderr=subprocess.PIPE,
1233
- text=True,
1234
- encoding="utf-8",
1235
- errors="replace",
1236
- )
1260
+ proc = subprocess.Popen(command, cwd=str(Path.cwd()), shell=shell, **popen_kwargs)
1237
1261
 
1238
1262
  try:
1239
1263
  stdout, stderr = proc.communicate(timeout=timeout)
1240
1264
  except subprocess.TimeoutExpired:
1241
- proc.terminate()
1265
+ # 超时后杀掉整个进程树,防止孤儿进程
1242
1266
  try:
1243
- proc.wait(timeout=5)
1244
- except subprocess.TimeoutExpired:
1245
- proc.kill()
1246
- proc.wait(timeout=5)
1267
+ if platform.system() == "Windows":
1268
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(proc.pid)], capture_output=True, timeout=5)
1269
+ else:
1270
+ os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
1271
+ proc.wait(timeout=3)
1272
+ if proc.poll() is None:
1273
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
1274
+ except Exception:
1275
+ pass
1276
+ finally:
1277
+ try: proc.kill()
1278
+ except: pass
1247
1279
  return -1, "", f"Command timed out after {timeout} seconds"
1248
1280
  except KeyboardInterrupt:
1249
- proc.terminate()
1281
+ # 处理用户 Ctrl+C 中断
1250
1282
  try:
1251
- proc.wait(timeout=5)
1252
- except subprocess.TimeoutExpired:
1253
- proc.kill()
1254
- proc.wait(timeout=5)
1283
+ if platform.system() == "Windows":
1284
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(proc.pid)], capture_output=True, timeout=5)
1285
+ else:
1286
+ os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
1287
+ except Exception:
1288
+ pass
1255
1289
  raise
1290
+
1256
1291
  return proc.returncode, stdout or "", stderr or ""
1257
1292
 
1258
1293
 
1294
+
1295
+ import os
1296
+ import signal
1297
+ import subprocess
1298
+ import uuid
1299
+ import time
1300
+ import platform
1301
+ from pathlib import Path
1302
+
1303
+ def handle_execute_command(args):
1304
+ command = args.get("command")
1305
+ if not command:
1306
+ return {"status": "error", "content": "Missing 'command' argument."}
1307
+
1308
+ cwd = resolve_execution_cwd(args.get("cwd"), Path.cwd())
1309
+
1310
+ # 解析命令类型参数
1311
+ is_background = args.get("background", False)
1312
+
1313
+ # ================= 路径 A: 常驻服务 (保留你原有的优秀逻辑) =================
1314
+ if is_background:
1315
+ output_log_path = args.get("output_log_path") or args.get("log_path")
1316
+ if not output_log_path:
1317
+ output_log_path = ROOT / "logs" / "background" / f"cmd_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{os.getpid()}.log"
1318
+
1319
+ result = start_background_process(command, cwd, timeout_seconds=None, output_log_path=output_log_path)
1320
+
1321
+ if result.get("status") == "ok":
1322
+ health_check_url = args.get("health_check_url")
1323
+ if health_check_url:
1324
+ try:
1325
+ ready = wait_for_health_check(str(health_check_url), timeout_seconds=int(args.get("health_check_timeout", 20)))
1326
+ result["health_check_ready"] = ready
1327
+ result["content"] = f"Started background process (pid={result['pid']})" + (" and health check succeeded." if ready else " but health check did not succeed yet.")
1328
+ except Exception:
1329
+ result["health_check_ready"] = False
1330
+ result["content"] = f"Started background process (pid={result['pid']})"
1331
+
1332
+ time.sleep(0.5) # 防止竞态
1333
+ result["output_tail"] = _read_text_file_tail(output_log_path, max_chars=4000)
1334
+ return result
1335
+ return result
1336
+
1337
+ # ================= 路径 B: 一次性命令 (新增 Sentinel 同步等待逻辑) =================
1338
+ else:
1339
+ timeout_seconds = int(args.get("timeout_seconds") or args.get("timeout") or 120)
1340
+
1341
+ # 生成唯一哨兵并组装命令 (跨平台适配错误码输出)
1342
+ sentinel = f"@@CMD_DONE_{uuid.uuid4().hex}@@"
1343
+ if platform.system() == "Windows":
1344
+ full_cmd = f"{command}\necho {sentinel} %errorlevel%\n"
1345
+ else:
1346
+ full_cmd = f"{command}\necho {sentinel} $?\n"
1347
+
1348
+ # 构建启动参数 (跨平台适配进程组)
1349
+ popen_kwargs = {
1350
+ "shell": True,
1351
+ "cwd": str(cwd),
1352
+ "stdout": subprocess.PIPE,
1353
+ "stderr": subprocess.STDOUT,
1354
+ "text": True,
1355
+ "bufsize": 1,
1356
+ }
1357
+
1358
+ if platform.system() == "Windows":
1359
+ popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
1360
+ else:
1361
+ popen_kwargs["start_new_session"] = True
1362
+
1363
+ process = subprocess.Popen(full_cmd, **popen_kwargs)
1364
+
1365
+ output_lines = []
1366
+ timed_out = False
1367
+ start_time = time.time()
1368
+
1369
+ try:
1370
+ while True:
1371
+ line = process.stdout.readline()
1372
+ if not line:
1373
+ if time.time() - start_time > timeout_seconds:
1374
+ timed_out = True
1375
+ break
1376
+ continue
1377
+
1378
+ if sentinel in line:
1379
+ # 提取退出码
1380
+ try:
1381
+ exit_code = int(line.split(sentinel)[-1].strip().rstrip())
1382
+ except:
1383
+ exit_code = -1
1384
+ break
1385
+ else:
1386
+ output_lines.append(line)
1387
+ if len(output_lines) > 5000:
1388
+ output_lines.pop(0)
1389
+
1390
+ if time.time() - start_time > timeout_seconds:
1391
+ timed_out = True
1392
+ break
1393
+ finally:
1394
+ # 超时或完成后,杀掉整个进程树 (跨平台适配)
1395
+ if process.poll() is None:
1396
+ try:
1397
+ if platform.system() == "Windows":
1398
+ # Windows: taskkill 强杀整个进程树
1399
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)],
1400
+ capture_output=True, timeout=5)
1401
+ else:
1402
+ # Unix: 杀整个进程组
1403
+ os.killpg(os.getpgid(process.pid), signal.SIGTERM)
1404
+ time.sleep(1)
1405
+ if process.poll() is None:
1406
+ os.killpg(os.getpgid(process.pid), signal.SIGKILL)
1407
+ except Exception:
1408
+ pass
1409
+
1410
+ content = "".join(output_lines)[-4000:]
1411
+
1412
+ if timed_out:
1413
+ return {
1414
+ "status": "timeout",
1415
+ "exit_code": None,
1416
+ "content": f"{content}\n[ERROR] Command timed out after {timeout_seconds} seconds and was forcefully terminated.\n[Hint] If this is a dev server, please use background=true."
1417
+ }
1418
+ else:
1419
+ return {
1420
+ "status": "success" if exit_code == 0 else "error",
1421
+ "exit_code": exit_code,
1422
+ "content": content
1423
+ }
1424
+
1425
+
1426
+
1259
1427
  def execute_tool_call(tool_call: Any) -> dict[str, Any]:
1260
1428
  name = getattr(tool_call.function, "name", "")
1261
1429
  args = safe_parse_tool_args(getattr(tool_call.function, "arguments", {}))
@@ -1454,46 +1622,6 @@ def execute_tool_call(tool_call: Any) -> dict[str, Any]:
1454
1622
  logger.info("list_dir result: %s", result)
1455
1623
  return result
1456
1624
 
1457
- if name == "execute_command":
1458
- command = args.get("command")
1459
- if not command:
1460
- result = {"status": "error", "content": t("tool_missing_arg", name="execute_command", arg="command")}
1461
- logger.error("execute_command missing command argument, raw args: %s", args)
1462
- return result
1463
- cwd = resolve_execution_cwd(args.get("cwd"), Path.cwd())
1464
-
1465
- timeout_seconds = None
1466
- timeout_value = args.get("timeout_seconds") or args.get("timeout")
1467
- if timeout_value is not None:
1468
- try:
1469
- timeout_seconds = int(timeout_value)
1470
- except (TypeError, ValueError):
1471
- timeout_seconds = None
1472
-
1473
- output_log_path = args.get("output_log_path") or args.get("log_path")
1474
- if not output_log_path:
1475
- output_log_path = ROOT / "logs" / "background" / f"cmd_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{os.getpid()}.log"
1476
- result = start_background_process(command, cwd, timeout_seconds=timeout_seconds, output_log_path=output_log_path)
1477
- if result.get("status") == "ok":
1478
- health_check_url = args.get("health_check_url")
1479
- if health_check_url:
1480
- try:
1481
- ready = wait_for_health_check(str(health_check_url), timeout_seconds=int(args.get("health_check_timeout", 20)))
1482
- result["health_check_ready"] = ready
1483
- result["content"] = (
1484
- f"Started background process (pid={result['pid']})"
1485
- + (" and confirmed health check succeeded." if ready else " but health check did not succeed yet.")
1486
- )
1487
- except Exception:
1488
- result["health_check_ready"] = False
1489
- result["content"] = f"Started background process (pid={result['pid']})"
1490
- result["output_tail"] = _read_text_file_tail(output_log_path, max_chars=4000)
1491
- logger.info("execute_command background result: %s", result)
1492
- return result
1493
-
1494
- logger.info("execute_command result: %s", result)
1495
- return result
1496
-
1497
1625
  if name == "execute_python_script":
1498
1626
  script = args.get("script")
1499
1627
  if not script:
@@ -1504,7 +1632,17 @@ def execute_tool_call(tool_call: Any) -> dict[str, Any]:
1504
1632
  script_path = Path(cwd) / "__cli_temp_script__.py"
1505
1633
  script_path.parent.mkdir(parents=True, exist_ok=True)
1506
1634
  script_path.write_text(script, encoding="utf-8")
1507
- returncode, stdout, stderr = run_subprocess_command([sys.executable, str(script_path)], cwd, timeout=TOOL_SUBPROCESS_TIMEOUT)
1635
+
1636
+ # 获取自定义超时,默认 120 秒,最大不超过 360 秒
1637
+ timeout_seconds = int(args.get("timeout_seconds") or 120)
1638
+ timeout_seconds = min(max(timeout_seconds, 10), TOOL_SUBPROCESS_TIMEOUT)
1639
+
1640
+ returncode, stdout, stderr = run_subprocess_command(
1641
+ [sys.executable, str(script_path)],
1642
+ cwd,
1643
+ shell=False,
1644
+ timeout=timeout_seconds
1645
+ )
1508
1646
  response = {
1509
1647
  "status": "ok" if returncode == 0 else "error",
1510
1648
  "content": stdout + stderr,
@@ -1513,6 +1651,7 @@ def execute_tool_call(tool_call: Any) -> dict[str, Any]:
1513
1651
  logger.info("execute_python_script result: %s", response)
1514
1652
  return response
1515
1653
 
1654
+
1516
1655
  if name in {item["function"]["name"] for item in ACTIVE_MCP_TOOL_DEFINITIONS}:
1517
1656
  logger.info("Executing MCP tool: %s, args: %s", name, args)
1518
1657
  return run_mcp_tool(name, args)
@@ -1776,8 +1915,10 @@ def plan_user_request(client: OpenAI, model: str, history: list[dict[str, Any]],
1776
1915
  if is_special_command(user_text):
1777
1916
  return user_text
1778
1917
 
1918
+ planning_prompt = PLANNING_PROMPT
1919
+
1779
1920
  planning_messages = [
1780
- {"role": "system", "content": PLANNING_PROMPT},
1921
+ {"role": "system", "content": planning_prompt},
1781
1922
  {"role": "user", "content": user_text},
1782
1923
  ]
1783
1924
  if history:
@@ -1960,9 +2101,7 @@ def handle_task_end_command(md_path: Path, client: OpenAI, model: str, system_pr
1960
2101
  compiled_text = "\n\n".join(["## Round " + r for r in selected])
1961
2102
 
1962
2103
  # Build messages for the model
1963
- sys_prompt = (
1964
- t("task_end_system_prompt")
1965
- )
2104
+ sys_prompt = t("task_end_system_prompt")
1966
2105
 
1967
2106
  user_msg = (
1968
2107
  t("task_end_user_message", compiled_text=compiled_text)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompt-copilot-cli
3
- Version: 0.2.2
3
+ Version: 0.2.6
4
4
  Summary: A lightweight terminal-based coding agent with file tools, command execution, multimodal image support, and MCP integration.
5
5
  Author: Jason Li
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "prompt-copilot-cli"
7
- version = "0.2.2"
7
+ version = "0.2.6"
8
8
  description = "A lightweight terminal-based coding agent with file tools, command execution, multimodal image support, and MCP integration."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,7 @@
1
1
  import contextlib
2
2
  import io
3
3
  import sys
4
+ import tempfile
4
5
  import types
5
6
  import unittest
6
7
  from pathlib import Path
@@ -102,6 +103,42 @@ class CLITests(unittest.TestCase):
102
103
  self.assertEqual(captured["messages"][0]["role"], "system")
103
104
  self.assertEqual(captured["messages"][-1]["content"], "帮我修复这个 bug")
104
105
 
106
+ def test_task_end_command_uses_aligned_working_principles(self) -> None:
107
+ captured: dict[str, object] = {}
108
+
109
+ def fake_chat_once(client, model, messages, temperature, debug_enabled=False):
110
+ captured["messages"] = messages
111
+ return types.SimpleNamespace(content="final prompt")
112
+
113
+ original_chat_once = main.chat_once
114
+ original_lang = main.UI_SYSTEM_LANGUAGE
115
+ main.UI_SYSTEM_LANGUAGE = "zh"
116
+ main.chat_once = fake_chat_once
117
+ try:
118
+ with tempfile.TemporaryDirectory() as temp_dir:
119
+ md_path = Path(temp_dir) / "recent_conversations.md"
120
+ md_path.write_text(
121
+ "# Recent conversations\n\n## Round 1\n\n**User:** /task-start\n\nhello\n\n**Assistant:** hi",
122
+ encoding="utf-8",
123
+ )
124
+ main.handle_task_end_command(
125
+ md_path=md_path,
126
+ client=object(),
127
+ model="gpt-4o-mini",
128
+ system_prompt="ignored",
129
+ workdir=Path(temp_dir),
130
+ debug_enabled=False,
131
+ )
132
+ finally:
133
+ main.UI_SYSTEM_LANGUAGE = original_lang
134
+ main.chat_once = original_chat_once
135
+
136
+ self.assertIn("messages", captured)
137
+ system_prompt = captured["messages"][0]["content"]
138
+ self.assertIn("先检查工作目录并理解需求", system_prompt)
139
+ self.assertIn("生成最终提示词时,请严格遵循以下约束", system_prompt)
140
+ self.assertIn("\n\n", system_prompt)
141
+
105
142
  def test_version_flag_prints_application_version_and_exits(self) -> None:
106
143
  parser = main.build_cli_parser()
107
144
  output = io.StringIO()