prompt-copilot-cli 0.2.6__tar.gz → 0.2.9__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.6/prompt_copilot_cli.egg-info → prompt_copilot_cli-0.2.9}/PKG-INFO +1 -1
  2. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/main.py +319 -115
  3. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9/prompt_copilot_cli.egg-info}/PKG-INFO +1 -1
  4. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/pyproject.toml +1 -1
  5. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/tests/test_cli_args.py +10 -0
  6. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/tests/test_file_tools.py +28 -0
  7. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/LICENSE +0 -0
  8. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/MANIFEST.in +0 -0
  9. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/README.md +0 -0
  10. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/README.zh-CN.md +0 -0
  11. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/prompt_copilot_cli.egg-info/SOURCES.txt +0 -0
  12. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/prompt_copilot_cli.egg-info/dependency_links.txt +0 -0
  13. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/prompt_copilot_cli.egg-info/entry_points.txt +0 -0
  14. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/prompt_copilot_cli.egg-info/requires.txt +0 -0
  15. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/prompt_copilot_cli.egg-info/top_level.txt +0 -0
  16. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/requirements.txt +0 -0
  17. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/setup.cfg +0 -0
  18. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/tests/test_command_timeout.py +0 -0
  19. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/tests/test_image_tool.py +0 -0
  20. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/tests/test_mcp_transport.py +0 -0
  21. {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.9}/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.6
3
+ Version: 0.2.9
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
@@ -88,118 +88,59 @@ def format_cumulative_token_summary() -> str:
88
88
  )
89
89
 
90
90
 
91
- def build_bottom_toolbar_text() -> str:
92
- base = t("toolbar_help")
93
- token_summary = format_cumulative_token_summary()
94
- return f"{token_summary} | {base}"
95
-
96
-
97
-
98
-
99
- DEFAULT_SYSTEM_WORKING_PRINCIPLES_ZH = """工作原则:
100
- 1、先检查工作目录并理解需求。
101
- 2、优先使用网络搜索类工具搜索网络实时信息,例如:天气、新闻、资讯等等。
102
- 3、所有代码文件生成、编辑、删除等操作均在工作目录中执行。如果用户没有指定具体目录,默认工作目录为工程的根目录。
103
- 4、如果需要,输出简洁的说明与下一步建议。
104
- 5、若涉及删除系统文件或执行危险命令,必须先向用户确认后方可执行。
105
- 6、当用户输入指令:/task-start 的时候,直接回复:请输入你的第一条初始提示。
106
- 7、遇到信息盲区时,严禁主观臆测,请优先使用搜索工具补齐信息缺口,确保回答精准有据。
107
- 8、如果用户想“读取/识别其他二进制文件”,你必须明确告知:我无法直接读取或理解通用二进制文件内容。
108
- 9、如果用户想“查看图片内容”,应优先调用图片读取工具将图片转成 base64 或视觉消息格式,再将其发送给支持多模态的模型。
109
- 10、对于图片类任务,优先使用图片工具而不是尝试直接读取二进制原始内容。
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 秒强制超时。
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。"""
115
-
116
- DEFAULT_SYSTEM_WORKING_PRINCIPLES_EN = """Working principles:
117
- 1. First inspect the working directory and understand the requirement.
118
- 2. Prefer using network search tools for real-time information, such as weather, news, or other current topics.
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.
120
- 4. If needed, provide concise explanations and next-step suggestions.
121
- 5. If the task involves deleting system files or executing dangerous commands, you must ask for confirmation first.
122
- 6. When the user enters /task-start, respond with: Please enter your first initial prompt.
123
- 7. When information is missing, do not speculate; prefer using search tools to fill the gap and keep the answer precise and evidence-based.
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.
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.
126
- 10. For image-related tasks, prefer the image tool rather than trying to read the raw binary contents directly.
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 字符以内。"""
91
+ def clear_task_memory_file() -> None:
92
+ MEMORY_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
93
+ MEMORY_FILE_PATH.write_text("# Task Execution Memory\n\n", encoding="utf-8")
144
94
 
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
95
 
147
- Please follow the same working principles and constraints as the default system prompt:
148
- {principles}
96
+ def append_task_memory_entry(text: str) -> None:
97
+ MEMORY_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
98
+ with MEMORY_FILE_PATH.open("a", encoding="utf-8") as f:
99
+ f.write(text)
100
+ if not text.endswith("\n"):
101
+ f.write("\n")
149
102
 
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
103
 
157
- ZH_SYSTEM_PROMPT = f"""
158
- 你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,专注于使用文件、命令、Python 脚本等工具完成开发任务。
159
-
160
- {DEFAULT_SYSTEM_WORKING_PRINCIPLES_ZH}
161
-
162
- """
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}
168
-
169
- """
170
-
171
-
172
- PLANNING_PROMPT = f"""
173
- 你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,能够通过使用文件、命令、Python 脚本等工具完成开发任务,当前你只能够生成任务执行计划。
174
-
175
- 注意:你的输出只能是“任务执行计划”的文本说明。绝对不能真实执行任何命令、脚本、文件修改或调用任何工具方法。你是在“规划”而非“动手”。
176
-
177
- 在生成任务执行计划时,请遵循以下{DEFAULT_SYSTEM_WORKING_PRINCIPLES_ZH}
104
+ def summarize_memory_value(value: Any, max_len: int = 240) -> str:
105
+ if isinstance(value, str):
106
+ if len(value) > max_len:
107
+ return value[: max_len - 20] + f"... (truncated, len={len(value)})"
108
+ return value
178
109
 
110
+ if isinstance(value, (dict, list)):
111
+ try:
112
+ serialized = json.dumps(value, ensure_ascii=False)
113
+ except Exception:
114
+ serialized = str(value)
115
+ if len(serialized) > max_len:
116
+ return serialized[: max_len - 20] + f"... (truncated, len={len(serialized)})"
117
+ return serialized
179
118
 
180
- 请严格按照以下格式输出结果:
119
+ return str(value)
181
120
 
182
- 用户原始指令:......
183
121
 
184
- 结合上下文得到用户的完整意图:.....
122
+ def summarize_tool_result(result: Any) -> str:
123
+ if isinstance(result, dict):
124
+ status = result.get("status")
125
+ content = result.get("content")
126
+ content_summary = summarize_memory_value(content, max_len=200)
127
+ extra_items = [
128
+ f"{k}={summarize_memory_value(v, max_len=80)}"
129
+ for k, v in result.items()
130
+ if k not in {"status", "content"}
131
+ ]
132
+ extra_text = f" | {", ".join(extra_items)}" if extra_items else ""
133
+ return f"status={status}{extra_text}\nContent: {content_summary}"
134
+ return summarize_memory_value(result)
185
135
 
186
- 接下来按照这个步骤逐步执行完成任务:
187
- 1、第一步:......
188
- 2、第二步:......
189
- ......
190
136
 
191
- """
137
+ def build_bottom_toolbar_text() -> str:
138
+ base = t("toolbar_help")
139
+ token_summary = format_cumulative_token_summary()
140
+ return f"{token_summary} | {base}"
192
141
 
193
142
 
194
143
  TRANSLATIONS = {
195
- "system_prompt": {
196
- "zh": ZH_SYSTEM_PROMPT,
197
- "en": EN_SYSTEM_PROMPT,
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
- },
203
144
  "config_field_model": {"zh": "模型名称,例如 qwen2.5-7b-instruct", "en": "Model name, for example qwen2.5-7b-instruct"},
204
145
  "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"},
205
146
  "config_field_api_key": {"zh": "API Key;若是本地模型可填写任意非空字符串", "en": "API key; for a local model you can enter any non-empty string"},
@@ -215,6 +156,8 @@ TRANSLATIONS = {
215
156
  "copy_file_desc": {"zh": "复制文件。", "en": "Copy a file."},
216
157
  "read_image_as_base64_desc": {"zh": "读取图片文件并将其编码为 base64。", "en": "Read an image file and encode it as base64."},
217
158
  "list_dir_desc": {"zh": "列出目录中的文件和子目录。", "en": "List files and subdirectories in the given directory."},
159
+ "search_code_desc": {"zh": "在目录中按文本模式搜索匹配内容。", "en": "Search for matching text within a directory."},
160
+ "edit_file_desc": {"zh": "在文件中替换指定文本。", "en": "Replace a specific string in a file."},
218
161
  "execute_command_desc": {"zh": "在指定目录下执行 shell 命令。", "en": "Execute a shell command in the specified directory."},
219
162
  "execute_python_script_desc": {"zh": "执行一个 Python 脚本或一段 Python 代码。", "en": "Execute a Python script or a block of Python code."},
220
163
  "path_desc": {"zh": "要读取的文件路径。", "en": "Path of the file to read."},
@@ -320,14 +263,37 @@ def t(key: str, **kwargs: Any) -> str:
320
263
  return text.format(**kwargs)
321
264
  return text
322
265
 
266
+ MEMORY_FILE_PATH = ROOT / "memory.md"
267
+
268
+ DEFAULT_SYSTEM_PROMPT = f"""
269
+ 你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,专注于使用文件、命令、Python 脚本等工具完成开发任务。
270
+
271
+ 工作原则:
272
+ 1、先检查工作目录并理解需求。
273
+ 2、优先使用网络搜索类工具搜索网络实时信息,例如:天气、新闻、资讯等等。
274
+ 3、所有代码文件生成、编辑、删除等操作均在工作目录中执行。如果用户没有指定具体目录,默认工作目录为工程的根目录。
275
+ 4、如果需要,输出简洁的说明与下一步建议。
276
+ 5、若涉及删除系统文件或执行危险命令,必须先向用户确认后方可执行。
277
+ 6、当用户输入指令:/task-start 的时候,直接回复:请输入你的第一条初始提示。
278
+ 7、遇到信息盲区时,严禁主观臆测,请优先使用搜索工具补齐信息缺口,确保回答精准有据。
279
+ 8、在任务处理中,工具调用会产生大量中间信息,而对话只保存有限几轮上下文,在生成最终结果或者是某些步骤需要回溯前面缺失的历史信息的时候可以读取记忆文件:{MEMORY_FILE_PATH}。
280
+ 9、如果用户想“读取/识别其他二进制文件”,你必须明确告知:我无法直接读取或理解通用二进制文件内容。
281
+ 10、如果用户想“查看图片内容”,应优先调用图片读取工具。
282
+ 11、当用户输入一条任务指令时,如果是详细的任务清单,你就理解用户要求逐步完成工作,同时执行每个步骤的时候明确说明当前环节与进度,实时反馈任务状态。
283
+ 12、使用 execute_python_script 时必须在脚本内自行管理进程生命周期:若是 ls、pytest、build 等短命令用 subprocess.run 显式设置 timeout,若是 npm run dev、flask run 等常驻服务用 subprocess.Popen 脱离终端启动并在脚本内轮询健康检查 URL,确认就绪后打印 PID 并立即 sys.exit() 退出以防触发 360 秒强制超时。
284
+ 13、无法调用官方联网检索工具而通过脚本抓取网络内容时需区分数据类型处理,抓取普通网页文本需剔除 HTML 标签、样式代码、广告碎片、无效注释、多余空行等冗余内容,仅留存有效正文且文本超出 8000 字符则截断,文本输出上限为 8000 字符,抓取程序源代码则无需过滤,完整保留原始代码、自带注释、缩进换行与原有格式,代码输出无字符数量限制。
285
+ 14、脱离终端启动常驻服务时务必兼顾跨平台兼容:Linux/Mac 环境设置 start_new_session=True,Windows 环境设置 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP。
286
+ 15、凡耗时不可控的超长任务(如大文件下载、模型训练、全量测试、复杂构建等),严禁在脚本内同步阻塞,必须用 subprocess.Popen 跨平台脱离终端启动并将进度心跳写入本地日志文件,打印 PID 后立即 sys.exit(),工具 timeout_seconds 设为 15 秒仅验证启动;后续须通过检查日志监控进度,若发现任务完成或长时间无心跳更新,必须主动调用系统命令(如 kill -9 PID)清理后台进程以防孤儿进程耗尽资源;仅在任务自身支持超时参数或耗时可预估时,方可在脚本内同步执行并设合理 timeout_seconds。
287
+ 16、如果需要搜索互联网信息时候优先使用中国国内搜索引擎优先,例如:bing、百度等。
288
+
289
+ """
323
290
 
324
291
  UI_SYSTEM_LANGUAGE = "en"
325
- APPLICATION_VERSION = "0.2.6"
326
- DEFAULT_SYSTEM_PROMPT = t("system_prompt")
292
+ APPLICATION_VERSION = "0.2.9"
327
293
  WORKSPACE_DIR = ROOT / "workspace"
328
294
  LOG_DIR = ROOT / "logs"
329
295
  LOG_FILE = LOG_DIR / "agent_runtime.log"
330
- DEFAULT_MAX_CHAT_COUNT = 7
296
+ DEFAULT_MAX_CHAT_COUNT = 6
331
297
  CHAT_MESSAGE_MAX_COUNT = 8
332
298
  CONFIG_SAVE_FILE_PATH = ROOT / "config.json"
333
299
  RE_ACTION_DELAY = 1 # unit: seconds
@@ -508,6 +474,39 @@ TOOL_DEFINITIONS = [
508
474
  },
509
475
  },
510
476
  },
477
+ {
478
+ "type": "function",
479
+ "function": {
480
+ "name": "search_code",
481
+ "description": t("search_code_desc"),
482
+ "parameters": {
483
+ "type": "object",
484
+ "properties": {
485
+ "path": {"type": "string", "description": t("list_dir_path_desc")},
486
+ "pattern": {"type": "string", "description": "Text pattern to search for."},
487
+ "recursive": {"type": "boolean", "description": t("list_dir_recursive_desc")},
488
+ "max_results": {"type": "integer", "description": "Maximum number of matches to return."}
489
+ },
490
+ "required": ["path", "pattern"],
491
+ },
492
+ },
493
+ },
494
+ {
495
+ "type": "function",
496
+ "function": {
497
+ "name": "edit_file",
498
+ "description": t("edit_file_desc"),
499
+ "parameters": {
500
+ "type": "object",
501
+ "properties": {
502
+ "path": {"type": "string", "description": t("write_path_desc")},
503
+ "old_string": {"type": "string", "description": "The text to replace."},
504
+ "new_string": {"type": "string", "description": "The replacement text."}
505
+ },
506
+ "required": ["path", "old_string", "new_string"],
507
+ },
508
+ },
509
+ },
511
510
  {
512
511
  "type": "function",
513
512
  "function": {
@@ -1622,6 +1621,94 @@ def execute_tool_call(tool_call: Any) -> dict[str, Any]:
1622
1621
  logger.info("list_dir result: %s", result)
1623
1622
  return result
1624
1623
 
1624
+ if name == "search_code":
1625
+ file_path = args.get("path") or args.get("file_path")
1626
+ pattern = args.get("pattern")
1627
+ if not file_path or not pattern:
1628
+ result = {"status": "error", "content": t("tool_missing_arg", name="search_code", arg="path/pattern")}
1629
+ logger.error("search_code missing arguments, raw args: %s", args)
1630
+ return result
1631
+
1632
+ path = Path(file_path).expanduser()
1633
+ if not path.is_absolute():
1634
+ path = Path.cwd() / path
1635
+ if not path.exists():
1636
+ result = {"status": "ok", "content": json.dumps([], ensure_ascii=False)}
1637
+ logger.info("search_code result: %s", result)
1638
+ return result
1639
+
1640
+ recursive = bool(args.get("recursive", True))
1641
+ max_results = args.get("max_results")
1642
+ try:
1643
+ max_results = int(max_results) if max_results is not None else 50
1644
+ except (TypeError, ValueError):
1645
+ max_results = 50
1646
+
1647
+ matches: list[dict[str, Any]] = []
1648
+ if path.is_file():
1649
+ candidates = [path]
1650
+ elif recursive:
1651
+ candidates = [item for item in sorted(path.rglob("*")) if item.is_file()]
1652
+ else:
1653
+ candidates = [item for item in sorted(path.iterdir()) if item.is_file()]
1654
+
1655
+ for candidate in candidates:
1656
+ try:
1657
+ content = candidate.read_text(encoding="utf-8")
1658
+ except (UnicodeDecodeError, OSError):
1659
+ continue
1660
+ for line_number, line in enumerate(content.splitlines(), start=1):
1661
+ if pattern in line:
1662
+ matches.append({
1663
+ "path": str(candidate.resolve()),
1664
+ "line": line_number,
1665
+ "content": line,
1666
+ })
1667
+ if len(matches) >= max_results:
1668
+ break
1669
+ if len(matches) >= max_results:
1670
+ break
1671
+
1672
+ result = {"status": "ok", "content": json.dumps(matches, ensure_ascii=False)}
1673
+ logger.info("search_code result: %s", result)
1674
+ return result
1675
+
1676
+ if name == "edit_file":
1677
+ file_path = args.get("path") or args.get("file_path")
1678
+ old_string = args.get("old_string")
1679
+ new_string = args.get("new_string")
1680
+ if not file_path or old_string is None or new_string is None:
1681
+ result = {"status": "error", "content": t("tool_missing_arg", name="edit_file", arg="path/old_string/new_string")}
1682
+ logger.error("edit_file missing arguments, raw args: %s", args)
1683
+ return result
1684
+
1685
+ path = Path(file_path).expanduser()
1686
+ if not path.is_absolute():
1687
+ path = Path.cwd() / path
1688
+ if not path.exists():
1689
+ result = {"status": "error", "content": f"File does not exist: {path}"}
1690
+ logger.info("edit_file result: %s", result)
1691
+ return result
1692
+
1693
+ try:
1694
+ original_text = path.read_text(encoding="utf-8")
1695
+ except Exception as exc:
1696
+ result = {"status": "error", "content": f"Failed to read file: {path}\n{exc}"}
1697
+ logger.exception("edit_file failed: %s", path)
1698
+ logger.info("edit_file result: %s", result)
1699
+ return result
1700
+
1701
+ if old_string not in original_text:
1702
+ result = {"status": "error", "content": f"Target text not found in file: {path}"}
1703
+ logger.info("edit_file result: %s", result)
1704
+ return result
1705
+
1706
+ updated_text = original_text.replace(old_string, new_string, 1)
1707
+ path.write_text(updated_text, encoding="utf-8")
1708
+ result = {"status": "ok", "content": f"Updated: {path}"}
1709
+ logger.info("edit_file result: %s", result)
1710
+ return result
1711
+
1625
1712
  if name == "execute_python_script":
1626
1713
  script = args.get("script")
1627
1714
  if not script:
@@ -1715,10 +1802,12 @@ def show_tool_result(tool_call: Any, result: dict[str, Any]) -> None:
1715
1802
  )
1716
1803
 
1717
1804
 
1718
- def chat_once(client: OpenAI, model: str, messages: list[dict[str, Any]], temperature: float, debug_enabled: bool = False) -> Any:
1805
+ def chat_once(client: OpenAI, model: str, messages: list[dict[str, Any]], temperature: float, debug_enabled: bool = False, disable_tools:bool = False) -> Any:
1719
1806
  ensure_not_interrupted()
1720
1807
  wait_for_model_call_interval()
1721
1808
  tool_definitions = TOOL_DEFINITIONS + ACTIVE_MCP_TOOL_DEFINITIONS
1809
+ if disable_tools:
1810
+ tool_definitions = []
1722
1811
  request_payload = {
1723
1812
  "model": model,
1724
1813
  "messages": messages,
@@ -1915,16 +2004,39 @@ def plan_user_request(client: OpenAI, model: str, history: list[dict[str, Any]],
1915
2004
  if is_special_command(user_text):
1916
2005
  return user_text
1917
2006
 
1918
- planning_prompt = PLANNING_PROMPT
2007
+ planning_prompt = f"""
2008
+ 你是一个编程行业需求分析专家,你能够通过历史上下文和用户的输入分析出用户的真实意图,能够拆分步骤,并且能够生成任务执行清单。
2009
+ 在任务处理中,工具调用会产生大量中间信息,而对话只保存有限几轮上下文,在生成最终结果或者是某些步骤需要回溯前面缺失的历史信息的时候可以读取记忆文件:{MEMORY_FILE_PATH}。
2010
+
2011
+ ### 请严格按照以下格式输出任务清单:
2012
+
2013
+ 用户原始指令:......
2014
+
2015
+ 结合上下文得到用户的完整意图:.....
2016
+
2017
+ 接下来按照这个步骤逐步执行完成任务:
2018
+ 1、第一步:......
2019
+ 2、第二步:......
2020
+ ......
2021
+
2022
+ """
2023
+
2024
+ user_prompt = f"""请结合历史上下文分析用户输入并生成任务清单。
2025
+ ### 历史上下文:
2026
+ {json.dumps(history, ensure_ascii=False)}
2027
+
2028
+ ### 用户输入:
2029
+ {user_text}
2030
+ """
1919
2031
 
1920
2032
  planning_messages = [
1921
2033
  {"role": "system", "content": planning_prompt},
1922
- {"role": "user", "content": user_text},
2034
+ {"role": "user", "content": user_prompt},
1923
2035
  ]
1924
- if history:
1925
- planning_messages.insert(1, {"role": "user", "content": "以下是历史上下文:\n" + json.dumps(history, ensure_ascii=False)})
2036
+ # if history:
2037
+ # planning_messages.insert(1, {"role": "user", "content": "以下是历史上下文:\n" + json.dumps(history, ensure_ascii=False)})
1926
2038
 
1927
- assistant_message = chat_once(client, model, planning_messages, temperature=0.2, debug_enabled=debug_enabled)
2039
+ assistant_message = chat_once(client, model, planning_messages, temperature=0.2, debug_enabled=debug_enabled, disable_tools=True)
1928
2040
  return (assistant_message.content or "").strip() or user_text
1929
2041
 
1930
2042
 
@@ -1938,6 +2050,9 @@ def is_special_command(user_text: str) -> bool:
1938
2050
 
1939
2051
  def run_agent(client: OpenAI, model: str, system_prompt: str, session_store: SessionStore, user_text: str, debug_enabled: bool = False, recorder: ConversationRecorder | None = None) -> None:
1940
2052
  reset_interruption_state()
2053
+ clear_task_memory_file()
2054
+ append_task_memory_entry(f"## Task started\n\nUser request: {summarize_memory_value(user_text, max_len=240)}\n\n")
2055
+
1941
2056
  history = session_store.load()
1942
2057
  planned_user_text = user_text
1943
2058
  if not is_special_command(user_text):
@@ -1946,6 +2061,8 @@ def run_agent(client: OpenAI, model: str, system_prompt: str, session_store: Ses
1946
2061
  except Exception:
1947
2062
  logger.exception("Planning step failed; continuing with original user input")
1948
2063
 
2064
+ append_task_memory_entry("## Task checklist\n\n" + summarize_memory_value(planned_user_text, max_len=10000) + "\n\n")
2065
+
1949
2066
  first_task_prompt = {"role": "user", "content": planned_user_text}
1950
2067
  history.append(first_task_prompt)
1951
2068
  session_store.save(history)
@@ -2021,6 +2138,12 @@ def run_agent(client: OpenAI, model: str, system_prompt: str, session_store: Ses
2021
2138
  result = execute_tool_call(tc)
2022
2139
  messages.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False)})
2023
2140
  show_tool_result(tc, result)
2141
+ append_task_memory_entry(
2142
+ "### Tool invocation\n\n"
2143
+ f"Tool: {tc.function.name}\n\n"
2144
+ f"Arguments: {summarize_memory_value(tc.function.arguments, max_len=1200)}\n\n"
2145
+ f"Result: {summarize_tool_result(result)}\n\n"
2146
+ )
2024
2147
  # end_calling_tips = f"tool={tc.function.name}\nresult={json.dumps(result, ensure_ascii=False)}"
2025
2148
  # show_stage(t("tool_call_finished"), end_calling_tips[:80])
2026
2149
 
@@ -2050,9 +2173,77 @@ def run_agent(client: OpenAI, model: str, system_prompt: str, session_store: Ses
2050
2173
  error_payload = {"status": "error", "content": traceback.format_exc()}
2051
2174
  messages.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(error_payload, ensure_ascii=False)})
2052
2175
  console.print(Panel.fit(traceback.format_exc(), title=t("tool_execution_error")))
2176
+ append_task_memory_entry(
2177
+ "### Tool invocation\n\n"
2178
+ f"Tool: {tc.function.name}\n\n"
2179
+ f"Arguments: {summarize_memory_value(tc.function.arguments, max_len=1200)}\n\n"
2180
+ f"Result: ERROR\n{summarize_tool_result(error_payload)}\n\n"
2181
+ )
2053
2182
  # if recorder:
2054
2183
  # recorder.record_error(traceback.format_exc())
2055
2184
 
2185
+ import json
2186
+ from typing import List, Union
2187
+
2188
+ def get_content_from_tool_calls(tool_calls: List) -> str:
2189
+ """
2190
+ Extract the `content` field from a list of tool call objects
2191
+ (as returned by OpenAI's ChatCompletionMessage.tool_calls).
2192
+
2193
+ Args:
2194
+ tool_calls: List[ChatCompletionMessageFunctionToolCall] or similar.
2195
+ Each item should have a `function.arguments` attribute
2196
+ which is a JSON string containing a dict with a `content` key.
2197
+
2198
+ Returns:
2199
+ The extracted `content` string if found, otherwise an empty string ("").
2200
+ """
2201
+ if not tool_calls:
2202
+ return ""
2203
+
2204
+ for tool_call in tool_calls:
2205
+ try:
2206
+ # 兼容 dict 形式(比如有些 mock / 测试数据)
2207
+ if isinstance(tool_call, dict):
2208
+ func = tool_call.get("function")
2209
+ if not func:
2210
+ continue
2211
+ # func 可能是 dict,也可能是有 arguments 属性的对象
2212
+ if isinstance(func, dict):
2213
+ arguments_str = func.get("arguments")
2214
+ else:
2215
+ arguments_str = getattr(func, "arguments", None)
2216
+ else:
2217
+ # OpenAI SDK 对象:ChatCompletionMessageFunctionToolCall
2218
+ func = getattr(tool_call, "function", None)
2219
+ if func is None:
2220
+ continue
2221
+ # Function.arguments 是一个 JSON 字符串
2222
+ arguments_str = getattr(func, "arguments", None)
2223
+
2224
+ if not arguments_str:
2225
+ continue
2226
+
2227
+ # arguments 本身就是 JSON 字符串,需要 parse
2228
+ if isinstance(arguments_str, str):
2229
+ arguments = json.loads(arguments_str)
2230
+ elif isinstance(arguments_str, dict):
2231
+ arguments = arguments_str
2232
+ else:
2233
+ # 其他类型直接放弃
2234
+ continue
2235
+
2236
+ # 真正取 content 字段
2237
+ if isinstance(arguments, dict) and "content" in arguments:
2238
+ return arguments["content"]
2239
+
2240
+ except (json.JSONDecodeError, TypeError, AttributeError):
2241
+ # 解析失败或结构异常,忽略这条 tool_call
2242
+ continue
2243
+
2244
+ return ""
2245
+
2246
+
2056
2247
 
2057
2248
  def handle_task_end_command(md_path: Path, client: OpenAI, model: str, system_prompt: str, workdir: Path, debug_enabled: bool = False) -> None:
2058
2249
  """Process recent_conversations.md to find the most recent /task-start and use the
@@ -2101,20 +2292,34 @@ def handle_task_end_command(md_path: Path, client: OpenAI, model: str, system_pr
2101
2292
  compiled_text = "\n\n".join(["## Round " + r for r in selected])
2102
2293
 
2103
2294
  # Build messages for the model
2104
- sys_prompt = t("task_end_system_prompt")
2295
+ sys_prompt = f"""
2296
+ 你是提示词优化专家,负责将对话记录整理为更清晰、可执行的最终任务提示词。
2105
2297
 
2106
- user_msg = (
2107
- t("task_end_user_message", compiled_text=compiled_text)
2108
- )
2298
+ ### 生成最终提示词时,请严格遵循以下约束:
2299
+ 1. 只输出最终改进后的提示词文本,不要添加任何解释、元信息或注释。
2300
+ 2. 保留用户原始目标、关键上下文、澄清或变更后的要求、待执行步骤与预期产物。
2301
+ 3. 语言必须清晰、可执行、步骤化,适合直接给后续执行器使用。
2302
+ 4. 如果上下文中存在不确定信息,应显式写成“待确认”或“需验证”的约束项。
2303
+ 5. 输出长度控制在 18000 字符以内。
2304
+
2305
+ """
2306
+
2307
+ user_prompt = f"""分析对话记录帮我生成最终提示词,把最终提示词内容输出给我。
2308
+ ### 对话记录
2309
+ {compiled_text}
2310
+ """
2109
2311
 
2110
2312
  try:
2111
- assistant_message = chat_once(client, model, [{"role": "system", "content": sys_prompt}, {"role": "user", "content": user_msg}], temperature=0.2, debug_enabled=debug_enabled)
2313
+ assistant_message = chat_once(client, model, [{"role": "system", "content": sys_prompt}, {"role": "user", "content": user_prompt}], temperature=0.2, debug_enabled=debug_enabled, disable_tools=True)
2112
2314
  except Exception:
2113
2315
  logger.exception(t("task_end_generation_failed_log"))
2114
2316
  console.print(Panel.fit(traceback.format_exc(), title=t("task_end_generate_failed")))
2115
2317
  return
2116
-
2318
+ prompt_call_tools = get_content_from_tool_calls(assistant_message.tool_calls)
2319
+ # console.print(f"{prompt_call_tools}")
2117
2320
  final_prompt = (assistant_message.content or "").strip()
2321
+ if not final_prompt:
2322
+ final_prompt = prompt_call_tools
2118
2323
  if not final_prompt:
2119
2324
  console.print(Panel.fit(t("task_end_no_prompt"), title=t("task_end_result")))
2120
2325
  return
@@ -2190,7 +2395,6 @@ def main() -> None:
2190
2395
 
2191
2396
  # Set localization language
2192
2397
  UI_SYSTEM_LANGUAGE = args.lang
2193
- DEFAULT_SYSTEM_PROMPT = t("system_prompt")
2194
2398
 
2195
2399
  # set up workspace directory
2196
2400
  WORKSPACE_DIR = Path(args.workdir)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompt-copilot-cli
3
- Version: 0.2.6
3
+ Version: 0.2.9
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.6"
7
+ version = "0.2.9"
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"
@@ -56,6 +56,16 @@ class CLITests(unittest.TestCase):
56
56
  self.assertIn("短时命令", joined)
57
57
  self.assertIn("持久命令", joined)
58
58
 
59
+ def test_system_prompt_includes_notebook_memory_guidance(self) -> None:
60
+ self.assertIn(
61
+ "记事本方式",
62
+ main.DEFAULT_SYSTEM_WORKING_PRINCIPLES_ZH,
63
+ )
64
+ self.assertIn(
65
+ "notebook-style",
66
+ main.DEFAULT_SYSTEM_WORKING_PRINCIPLES_EN,
67
+ )
68
+
59
69
  def test_special_commands_skip_planning(self) -> None:
60
70
  called = {"value": False}
61
71
 
@@ -87,6 +87,34 @@ class FileToolTests(unittest.TestCase):
87
87
  self.assertTrue(dest.exists())
88
88
  self.assertEqual(dest.read_text(encoding="utf-8"), "hello")
89
89
 
90
+ def test_search_code_tool_finds_pattern_in_file(self) -> None:
91
+ with tempfile.TemporaryDirectory() as tmpdir:
92
+ root = Path(tmpdir)
93
+ target = root / "demo.py"
94
+ target.write_text("def greet():\n return 'hello'\n", encoding="utf-8")
95
+
96
+ result = main.execute_tool_call(DummyToolCall("search_code", {"path": str(root), "pattern": "return 'hello'"}))
97
+
98
+ self.assertEqual(result["status"], "ok")
99
+ matches = json.loads(result["content"])
100
+ self.assertTrue(matches)
101
+ self.assertEqual(matches[0]["path"], str(target))
102
+
103
+ def test_edit_file_tool_replaces_text(self) -> None:
104
+ with tempfile.TemporaryDirectory() as tmpdir:
105
+ target = Path(tmpdir) / "demo.py"
106
+ target.write_text("print('before')\n", encoding="utf-8")
107
+
108
+ result = main.execute_tool_call(
109
+ DummyToolCall(
110
+ "edit_file",
111
+ {"path": str(target), "old_string": "before", "new_string": "after"},
112
+ )
113
+ )
114
+
115
+ self.assertEqual(result["status"], "ok")
116
+ self.assertEqual(target.read_text(encoding="utf-8"), "print('after')\n")
117
+
90
118
 
91
119
  if __name__ == "__main__":
92
120
  unittest.main()