prompt-copilot-cli 0.2.2__tar.gz → 0.2.3__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.
- {prompt_copilot_cli-0.2.2/prompt_copilot_cli.egg-info → prompt_copilot_cli-0.2.3}/PKG-INFO +1 -1
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/main.py +244 -122
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3/prompt_copilot_cli.egg-info}/PKG-INFO +1 -1
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/pyproject.toml +1 -1
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/LICENSE +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/MANIFEST.in +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/README.md +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/README.zh-CN.md +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/prompt_copilot_cli.egg-info/SOURCES.txt +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/prompt_copilot_cli.egg-info/dependency_links.txt +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/prompt_copilot_cli.egg-info/entry_points.txt +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/prompt_copilot_cli.egg-info/requires.txt +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/prompt_copilot_cli.egg-info/top_level.txt +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/requirements.txt +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/setup.cfg +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/tests/test_cli_args.py +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/tests/test_command_timeout.py +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/tests/test_file_tools.py +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/tests/test_image_tool.py +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/tests/test_mcp_transport.py +0 -0
- {prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/tests/test_multimodal_messages.py +0 -0
|
@@ -94,6 +94,8 @@ def build_bottom_toolbar_text() -> str:
|
|
|
94
94
|
return f"{token_summary} | {base}"
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
|
|
98
|
+
|
|
97
99
|
ZH_SYSTEM_PROMPT = """
|
|
98
100
|
你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,专注于使用文件、命令、Python 脚本等工具完成开发任务。
|
|
99
101
|
|
|
@@ -109,8 +111,10 @@ ZH_SYSTEM_PROMPT = """
|
|
|
109
111
|
9、如果用户想“查看图片内容”,应优先调用图片读取工具将图片转成 base64 或视觉消息格式,再将其发送给支持多模态的模型。
|
|
110
112
|
10、对于图片类任务,优先使用图片工具而不是尝试直接读取二进制原始内容。
|
|
111
113
|
11、当用户输入一条任务指令时,如果是详细的任务清单,你就理解用户要求逐步完成工作,同时每个步骤执行完毕要反馈状态和进度。
|
|
112
|
-
12
|
|
114
|
+
12、使用 execute_python_script 时必须在脚本内自行管理进程生命周期:若是 ls、pytest、build 等短命令用 subprocess.run 显式设置 timeout,若是 npm run dev、flask run 等常驻服务用 subprocess.Popen 脱离终端启动并在脚本内轮询健康检查 URL,确认就绪后打印 PID 并立即 sys.exit() 退出以防触发 360 秒强制超时。
|
|
113
115
|
13、无法调用官方联网检索工具而通过脚本抓取网络内容时需区分数据类型处理,抓取普通网页文本需剔除 HTML 标签、样式代码、广告碎片、无效注释、多余空行等冗余内容,仅留存有效正文且文本超出 8000 字符则截断,文本输出上限为 8000 字符,抓取程序源代码则无需过滤,完整保留原始代码、自带注释、缩进换行与原有格式,代码输出无字符数量限制。
|
|
116
|
+
14、脱离终端启动常驻服务时务必兼顾跨平台兼容:Linux/Mac 环境设置 start_new_session=True,Windows 环境设置 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP。
|
|
117
|
+
15、凡耗时不可控的超长任务(如大文件下载、模型训练、全量测试、复杂构建等),严禁在脚本内同步阻塞,必须用 subprocess.Popen 跨平台脱离终端启动并将进度心跳写入本地日志文件,打印 PID 后立即 sys.exit(),工具 timeout_seconds 设为 15 秒仅验证启动;后续须通过检查日志监控进度,若发现任务完成或长时间无心跳更新,必须主动调用系统命令(如 kill -9 PID)清理后台进程以防孤儿进程耗尽资源;仅在任务自身支持超时参数或耗时可预估时,方可在脚本内同步执行并设合理 timeout_seconds。
|
|
114
118
|
|
|
115
119
|
"""
|
|
116
120
|
|
|
@@ -129,36 +133,48 @@ Working principles:
|
|
|
129
133
|
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
134
|
10. For image-related tasks, prefer the image tool rather than trying to read the raw binary contents directly.
|
|
131
135
|
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.
|
|
136
|
+
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.
|
|
133
137
|
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.
|
|
138
|
+
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.
|
|
139
|
+
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.
|
|
134
140
|
|
|
135
141
|
"""
|
|
136
142
|
|
|
137
143
|
|
|
138
|
-
PLANNING_PROMPT =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
144
|
+
PLANNING_PROMPT = """
|
|
145
|
+
你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,专注于使用文件、命令、Python 脚本等工具完成开发任务。
|
|
146
|
+
|
|
147
|
+
在生成任务执行计划时,请遵循以下工作原则:
|
|
148
|
+
|
|
149
|
+
1、先检查工作目录并理解需求。
|
|
150
|
+
2、优先使用网络搜索类工具搜索网络实时信息,例如:天气、新闻、资讯等等。
|
|
151
|
+
3、所有代码文件生成、编辑、删除等操作均在工作目录中执行。如果用户没有指定具体目录,默认工作目录为工程的根目录。
|
|
152
|
+
4、如果需要,输出简洁的说明与下一步建议。
|
|
153
|
+
5、若涉及删除系统文件或执行危险命令,必须先向用户确认后方可执行。
|
|
154
|
+
6、当用户输入指令:/task-start 的时候,直接回复:请输入你的第一条初始提示。
|
|
155
|
+
7、遇到信息盲区时,严禁主观臆测,请优先使用搜索工具补齐信息缺口,确保回答精准有据。
|
|
156
|
+
8、如果用户想“读取/识别其他二进制文件”,你必须明确告知:我无法直接读取或理解通用二进制文件内容。
|
|
157
|
+
9、如果用户想“查看图片内容”,应优先调用图片读取工具将图片转成 base64 或视觉消息格式,再将其发送给支持多模态的模型。
|
|
158
|
+
10、对于图片类任务,优先使用图片工具而不是尝试直接读取二进制原始内容。
|
|
159
|
+
11、当用户输入一条任务指令时,如果是详细的任务清单,你就理解用户要求逐步完成工作,同时每个步骤执行完毕要反馈状态和进度。
|
|
160
|
+
12、使用 execute_python_script 时必须在脚本内自行管理进程生命周期:若是 ls、pytest、build 等短命令用 subprocess.run 显式设置 timeout,若是 npm run dev、flask run 等常驻服务用 subprocess.Popen 脱离终端启动并在脚本内轮询健康检查 URL,确认就绪后打印 PID 并立即 sys.exit() 退出以防触发 360 秒强制超时。
|
|
161
|
+
13、无法调用官方联网检索工具而通过脚本抓取网络内容时需区分数据类型处理,抓取普通网页文本需剔除 HTML 标签、样式代码、广告碎片、无效注释、多余空行等冗余内容,仅留存有效正文且文本超出 8000 字符则截断,文本输出上限为 8000 字符,抓取程序源代码则无需过滤,完整保留原始代码、自带注释、缩进换行与原有格式,代码输出无字符数量限制。
|
|
162
|
+
14、脱离终端启动常驻服务时务必兼顾跨平台兼容:Linux/Mac 环境设置 start_new_session=True,Windows 环境设置 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP。
|
|
163
|
+
15、凡耗时不可控的超长任务(如大文件下载、模型训练、全量测试、复杂构建等),严禁在脚本内同步阻塞,必须用 subprocess.Popen 跨平台脱离终端启动并将进度心跳写入本地日志文件,打印 PID 后立即 sys.exit(),工具 timeout_seconds 设为 15 秒仅验证启动;后续须通过检查日志监控进度,若发现任务完成或长时间无心跳更新,必须主动调用系统命令(如 kill -9 PID)清理后台进程以防孤儿进程耗尽资源;仅在任务自身支持超时参数或耗时可预估时,方可在脚本内同步执行并设合理 timeout_seconds。
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
请严格按照以下格式输出结果:
|
|
167
|
+
|
|
168
|
+
用户原始指令:......
|
|
169
|
+
|
|
170
|
+
结合上下文得到用户的完整意图:.....
|
|
171
|
+
|
|
172
|
+
接下来按照这个步骤逐步执行完成任务:
|
|
173
|
+
1、第一步:......
|
|
174
|
+
2、第二步:......
|
|
175
|
+
......
|
|
176
|
+
|
|
177
|
+
"""
|
|
162
178
|
|
|
163
179
|
|
|
164
180
|
TRANSLATIONS = {
|
|
@@ -289,7 +305,7 @@ def t(key: str, **kwargs: Any) -> str:
|
|
|
289
305
|
|
|
290
306
|
|
|
291
307
|
UI_SYSTEM_LANGUAGE = "en"
|
|
292
|
-
APPLICATION_VERSION = "0.2.
|
|
308
|
+
APPLICATION_VERSION = "0.2.3"
|
|
293
309
|
DEFAULT_SYSTEM_PROMPT = t("system_prompt")
|
|
294
310
|
WORKSPACE_DIR = ROOT / "workspace"
|
|
295
311
|
LOG_DIR = ROOT / "logs"
|
|
@@ -475,38 +491,31 @@ TOOL_DEFINITIONS = [
|
|
|
475
491
|
},
|
|
476
492
|
},
|
|
477
493
|
},
|
|
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
494
|
{
|
|
496
495
|
"type": "function",
|
|
497
496
|
"function": {
|
|
498
497
|
"name": "execute_python_script",
|
|
499
|
-
"description":
|
|
498
|
+
"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
499
|
"parameters": {
|
|
501
500
|
"type": "object",
|
|
502
501
|
"properties": {
|
|
503
|
-
"script": {
|
|
504
|
-
|
|
502
|
+
"script": {
|
|
503
|
+
"type": "string",
|
|
504
|
+
"description": "The Python script content to execute."
|
|
505
|
+
},
|
|
506
|
+
"cwd": {
|
|
507
|
+
"type": "string",
|
|
508
|
+
"description": "The working directory for the script execution."
|
|
509
|
+
},
|
|
510
|
+
"timeout_seconds": {
|
|
511
|
+
"type": "integer",
|
|
512
|
+
"description": "Maximum execution time in seconds before the process is terminated. Defaults to 120. Set a shorter timeout for quick commands to avoid waiting."
|
|
513
|
+
}
|
|
505
514
|
},
|
|
506
|
-
"required": ["script", "cwd"]
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
515
|
+
"required": ["script", "cwd"]
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
510
519
|
]
|
|
511
520
|
ACTIVE_MCP_TOOL_DEFINITIONS: list[dict[str, Any]] = []
|
|
512
521
|
ACTIVE_MCP_TOOL_CONFIG: dict[str, Any] = {}
|
|
@@ -1211,51 +1220,193 @@ def wait_for_health_check(url: str, timeout_seconds: int = 20) -> bool:
|
|
|
1211
1220
|
|
|
1212
1221
|
def run_subprocess_command(command: Any, cwd: str, shell: bool = False, timeout: int | None = None) -> tuple[int, str, str]:
|
|
1213
1222
|
safe_cwd = resolve_execution_cwd(cwd, Path.cwd())
|
|
1223
|
+
|
|
1224
|
+
# 跨平台进程隔离配置
|
|
1225
|
+
popen_kwargs = {
|
|
1226
|
+
"cwd": safe_cwd,
|
|
1227
|
+
"stdout": subprocess.PIPE,
|
|
1228
|
+
"stderr": subprocess.PIPE,
|
|
1229
|
+
"stdin": subprocess.DEVNULL, # 根治等待输入导致的卡死!
|
|
1230
|
+
"text": True,
|
|
1231
|
+
"encoding": "utf-8",
|
|
1232
|
+
"errors": "replace",
|
|
1233
|
+
}
|
|
1234
|
+
if platform.system() == "Windows":
|
|
1235
|
+
popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
1236
|
+
else:
|
|
1237
|
+
popen_kwargs["start_new_session"] = True # 创建新进程组,便于后续杀整棵树
|
|
1238
|
+
|
|
1214
1239
|
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
|
-
)
|
|
1240
|
+
proc = subprocess.Popen(command, shell=shell, **popen_kwargs)
|
|
1225
1241
|
except (FileNotFoundError, NotADirectoryError, OSError) as exc:
|
|
1226
1242
|
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
|
-
)
|
|
1243
|
+
proc = subprocess.Popen(command, cwd=str(Path.cwd()), shell=shell, **popen_kwargs)
|
|
1237
1244
|
|
|
1238
1245
|
try:
|
|
1239
1246
|
stdout, stderr = proc.communicate(timeout=timeout)
|
|
1240
1247
|
except subprocess.TimeoutExpired:
|
|
1241
|
-
|
|
1248
|
+
# 超时后杀掉整个进程树,防止孤儿进程
|
|
1242
1249
|
try:
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1250
|
+
if platform.system() == "Windows":
|
|
1251
|
+
subprocess.run(["taskkill", "/F", "/T", "/PID", str(proc.pid)], capture_output=True, timeout=5)
|
|
1252
|
+
else:
|
|
1253
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
1254
|
+
proc.wait(timeout=3)
|
|
1255
|
+
if proc.poll() is None:
|
|
1256
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
1257
|
+
except Exception:
|
|
1258
|
+
pass
|
|
1259
|
+
finally:
|
|
1260
|
+
try: proc.kill()
|
|
1261
|
+
except: pass
|
|
1247
1262
|
return -1, "", f"Command timed out after {timeout} seconds"
|
|
1248
1263
|
except KeyboardInterrupt:
|
|
1249
|
-
|
|
1264
|
+
# 处理用户 Ctrl+C 中断
|
|
1250
1265
|
try:
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1266
|
+
if platform.system() == "Windows":
|
|
1267
|
+
subprocess.run(["taskkill", "/F", "/T", "/PID", str(proc.pid)], capture_output=True, timeout=5)
|
|
1268
|
+
else:
|
|
1269
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
1270
|
+
except Exception:
|
|
1271
|
+
pass
|
|
1255
1272
|
raise
|
|
1273
|
+
|
|
1256
1274
|
return proc.returncode, stdout or "", stderr or ""
|
|
1257
1275
|
|
|
1258
1276
|
|
|
1277
|
+
|
|
1278
|
+
import os
|
|
1279
|
+
import signal
|
|
1280
|
+
import subprocess
|
|
1281
|
+
import uuid
|
|
1282
|
+
import time
|
|
1283
|
+
import platform
|
|
1284
|
+
from pathlib import Path
|
|
1285
|
+
|
|
1286
|
+
def handle_execute_command(args):
|
|
1287
|
+
command = args.get("command")
|
|
1288
|
+
if not command:
|
|
1289
|
+
return {"status": "error", "content": "Missing 'command' argument."}
|
|
1290
|
+
|
|
1291
|
+
cwd = resolve_execution_cwd(args.get("cwd"), Path.cwd())
|
|
1292
|
+
|
|
1293
|
+
# 解析命令类型参数
|
|
1294
|
+
is_background = args.get("background", False)
|
|
1295
|
+
|
|
1296
|
+
# ================= 路径 A: 常驻服务 (保留你原有的优秀逻辑) =================
|
|
1297
|
+
if is_background:
|
|
1298
|
+
output_log_path = args.get("output_log_path") or args.get("log_path")
|
|
1299
|
+
if not output_log_path:
|
|
1300
|
+
output_log_path = ROOT / "logs" / "background" / f"cmd_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{os.getpid()}.log"
|
|
1301
|
+
|
|
1302
|
+
result = start_background_process(command, cwd, timeout_seconds=None, output_log_path=output_log_path)
|
|
1303
|
+
|
|
1304
|
+
if result.get("status") == "ok":
|
|
1305
|
+
health_check_url = args.get("health_check_url")
|
|
1306
|
+
if health_check_url:
|
|
1307
|
+
try:
|
|
1308
|
+
ready = wait_for_health_check(str(health_check_url), timeout_seconds=int(args.get("health_check_timeout", 20)))
|
|
1309
|
+
result["health_check_ready"] = ready
|
|
1310
|
+
result["content"] = f"Started background process (pid={result['pid']})" + (" and health check succeeded." if ready else " but health check did not succeed yet.")
|
|
1311
|
+
except Exception:
|
|
1312
|
+
result["health_check_ready"] = False
|
|
1313
|
+
result["content"] = f"Started background process (pid={result['pid']})"
|
|
1314
|
+
|
|
1315
|
+
time.sleep(0.5) # 防止竞态
|
|
1316
|
+
result["output_tail"] = _read_text_file_tail(output_log_path, max_chars=4000)
|
|
1317
|
+
return result
|
|
1318
|
+
return result
|
|
1319
|
+
|
|
1320
|
+
# ================= 路径 B: 一次性命令 (新增 Sentinel 同步等待逻辑) =================
|
|
1321
|
+
else:
|
|
1322
|
+
timeout_seconds = int(args.get("timeout_seconds") or args.get("timeout") or 120)
|
|
1323
|
+
|
|
1324
|
+
# 生成唯一哨兵并组装命令 (跨平台适配错误码输出)
|
|
1325
|
+
sentinel = f"@@CMD_DONE_{uuid.uuid4().hex}@@"
|
|
1326
|
+
if platform.system() == "Windows":
|
|
1327
|
+
full_cmd = f"{command}\necho {sentinel} %errorlevel%\n"
|
|
1328
|
+
else:
|
|
1329
|
+
full_cmd = f"{command}\necho {sentinel} $?\n"
|
|
1330
|
+
|
|
1331
|
+
# 构建启动参数 (跨平台适配进程组)
|
|
1332
|
+
popen_kwargs = {
|
|
1333
|
+
"shell": True,
|
|
1334
|
+
"cwd": str(cwd),
|
|
1335
|
+
"stdout": subprocess.PIPE,
|
|
1336
|
+
"stderr": subprocess.STDOUT,
|
|
1337
|
+
"text": True,
|
|
1338
|
+
"bufsize": 1,
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if platform.system() == "Windows":
|
|
1342
|
+
popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
1343
|
+
else:
|
|
1344
|
+
popen_kwargs["start_new_session"] = True
|
|
1345
|
+
|
|
1346
|
+
process = subprocess.Popen(full_cmd, **popen_kwargs)
|
|
1347
|
+
|
|
1348
|
+
output_lines = []
|
|
1349
|
+
timed_out = False
|
|
1350
|
+
start_time = time.time()
|
|
1351
|
+
|
|
1352
|
+
try:
|
|
1353
|
+
while True:
|
|
1354
|
+
line = process.stdout.readline()
|
|
1355
|
+
if not line:
|
|
1356
|
+
if time.time() - start_time > timeout_seconds:
|
|
1357
|
+
timed_out = True
|
|
1358
|
+
break
|
|
1359
|
+
continue
|
|
1360
|
+
|
|
1361
|
+
if sentinel in line:
|
|
1362
|
+
# 提取退出码
|
|
1363
|
+
try:
|
|
1364
|
+
exit_code = int(line.split(sentinel)[-1].strip().rstrip())
|
|
1365
|
+
except:
|
|
1366
|
+
exit_code = -1
|
|
1367
|
+
break
|
|
1368
|
+
else:
|
|
1369
|
+
output_lines.append(line)
|
|
1370
|
+
if len(output_lines) > 5000:
|
|
1371
|
+
output_lines.pop(0)
|
|
1372
|
+
|
|
1373
|
+
if time.time() - start_time > timeout_seconds:
|
|
1374
|
+
timed_out = True
|
|
1375
|
+
break
|
|
1376
|
+
finally:
|
|
1377
|
+
# 超时或完成后,杀掉整个进程树 (跨平台适配)
|
|
1378
|
+
if process.poll() is None:
|
|
1379
|
+
try:
|
|
1380
|
+
if platform.system() == "Windows":
|
|
1381
|
+
# Windows: taskkill 强杀整个进程树
|
|
1382
|
+
subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)],
|
|
1383
|
+
capture_output=True, timeout=5)
|
|
1384
|
+
else:
|
|
1385
|
+
# Unix: 杀整个进程组
|
|
1386
|
+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
1387
|
+
time.sleep(1)
|
|
1388
|
+
if process.poll() is None:
|
|
1389
|
+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
1390
|
+
except Exception:
|
|
1391
|
+
pass
|
|
1392
|
+
|
|
1393
|
+
content = "".join(output_lines)[-4000:]
|
|
1394
|
+
|
|
1395
|
+
if timed_out:
|
|
1396
|
+
return {
|
|
1397
|
+
"status": "timeout",
|
|
1398
|
+
"exit_code": None,
|
|
1399
|
+
"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."
|
|
1400
|
+
}
|
|
1401
|
+
else:
|
|
1402
|
+
return {
|
|
1403
|
+
"status": "success" if exit_code == 0 else "error",
|
|
1404
|
+
"exit_code": exit_code,
|
|
1405
|
+
"content": content
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
|
|
1259
1410
|
def execute_tool_call(tool_call: Any) -> dict[str, Any]:
|
|
1260
1411
|
name = getattr(tool_call.function, "name", "")
|
|
1261
1412
|
args = safe_parse_tool_args(getattr(tool_call.function, "arguments", {}))
|
|
@@ -1454,46 +1605,6 @@ def execute_tool_call(tool_call: Any) -> dict[str, Any]:
|
|
|
1454
1605
|
logger.info("list_dir result: %s", result)
|
|
1455
1606
|
return result
|
|
1456
1607
|
|
|
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
1608
|
if name == "execute_python_script":
|
|
1498
1609
|
script = args.get("script")
|
|
1499
1610
|
if not script:
|
|
@@ -1504,7 +1615,17 @@ def execute_tool_call(tool_call: Any) -> dict[str, Any]:
|
|
|
1504
1615
|
script_path = Path(cwd) / "__cli_temp_script__.py"
|
|
1505
1616
|
script_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1506
1617
|
script_path.write_text(script, encoding="utf-8")
|
|
1507
|
-
|
|
1618
|
+
|
|
1619
|
+
# 获取自定义超时,默认 120 秒,最大不超过 360 秒
|
|
1620
|
+
timeout_seconds = int(args.get("timeout_seconds") or 120)
|
|
1621
|
+
timeout_seconds = min(max(timeout_seconds, 10), TOOL_SUBPROCESS_TIMEOUT)
|
|
1622
|
+
|
|
1623
|
+
returncode, stdout, stderr = run_subprocess_command(
|
|
1624
|
+
[sys.executable, str(script_path)],
|
|
1625
|
+
cwd,
|
|
1626
|
+
shell=False,
|
|
1627
|
+
timeout=timeout_seconds
|
|
1628
|
+
)
|
|
1508
1629
|
response = {
|
|
1509
1630
|
"status": "ok" if returncode == 0 else "error",
|
|
1510
1631
|
"content": stdout + stderr,
|
|
@@ -1513,6 +1634,7 @@ def execute_tool_call(tool_call: Any) -> dict[str, Any]:
|
|
|
1513
1634
|
logger.info("execute_python_script result: %s", response)
|
|
1514
1635
|
return response
|
|
1515
1636
|
|
|
1637
|
+
|
|
1516
1638
|
if name in {item["function"]["name"] for item in ACTIVE_MCP_TOOL_DEFINITIONS}:
|
|
1517
1639
|
logger.info("Executing MCP tool: %s, args: %s", name, args)
|
|
1518
1640
|
return run_mcp_tool(name, args)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "prompt-copilot-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/prompt_copilot_cli.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/prompt_copilot_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/prompt_copilot_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{prompt_copilot_cli-0.2.2 → prompt_copilot_cli-0.2.3}/prompt_copilot_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|