jarvis-ai-assistant 0.1.52__py3-none-any.whl → 0.1.53__py3-none-any.whl

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.

Potentially problematic release.


This version of jarvis-ai-assistant might be problematic. Click here for more details.

jarvis/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Jarvis AI Assistant"""
2
2
 
3
- __version__ = "0.1.52"
3
+ __version__ = "0.1.53"
jarvis/models/ai8.py CHANGED
@@ -71,6 +71,7 @@ class AI8Model(BasePlatform):
71
71
 
72
72
  def set_model_name(self, model_name: str):
73
73
  """设置模型名称"""
74
+ PrettyOutput.print(f"设置模型: {model_name}", OutputType.USER)
74
75
  self.model_name = model_name
75
76
 
76
77
  def create_conversation(self) -> bool:
jarvis/models/openai.py CHANGED
@@ -43,6 +43,7 @@ class OpenAIModel(BasePlatform):
43
43
 
44
44
  def set_model_name(self, model_name: str):
45
45
  """设置模型名称"""
46
+ PrettyOutput.print(f"设置模型: {model_name}", OutputType.USER)
46
47
  self.model_name = model_name
47
48
 
48
49
  def set_system_message(self, message: str):
jarvis/models/oyi.py CHANGED
@@ -44,6 +44,7 @@ class OyiModel(BasePlatform):
44
44
 
45
45
  def set_model_name(self, model_name: str):
46
46
  """设置模型名称"""
47
+ PrettyOutput.print(f"设置模型: {model_name}", OutputType.USER)
47
48
  self.model_name = model_name
48
49
 
49
50
 
jarvis/tools/code_edit.py CHANGED
@@ -2,15 +2,18 @@ import hashlib
2
2
  import os
3
3
  import re
4
4
  import sqlite3
5
- from typing import Dict, Any, List, Optional
5
+ import time
6
+ from typing import Dict, Any, List, Optional, Tuple
6
7
 
7
8
  import yaml
8
- from jarvis.utils import OutputType, PrettyOutput
9
+ from jarvis.models.base import BasePlatform
10
+ from jarvis.utils import OutputType, PrettyOutput, get_multiline_input, load_env_from_file
9
11
  from jarvis.models.registry import PlatformRegistry
10
12
 
13
+
11
14
  class CodeEditTool:
12
15
  """代码修改工具"""
13
-
16
+
14
17
  name = "code_edit"
15
18
  description = "根据需求描述修改代码文件"
16
19
  parameters = {
@@ -31,7 +34,7 @@ class CodeEditTool:
31
34
  },
32
35
  "required": ["feature", "root_dir", "language"]
33
36
  }
34
-
37
+
35
38
  def __init__(self):
36
39
  """初始化代码修改工具"""
37
40
  self.main_model = self._new_model()
@@ -58,10 +61,12 @@ class CodeEditTool:
58
61
  self.root_dir = ""
59
62
  self.language = ""
60
63
  self.current_dir = ""
64
+
61
65
  def _new_model(self):
62
66
  """获取大模型"""
63
- platform_name = os.environ.get("JARVIS_CODEGEN_PLATFORM", PlatformRegistry.global_platform_name)
64
- model_name = os.environ.get("JARVIS_CODEGEN_MODEL")
67
+ platform_name = os.environ.get(
68
+ "JARVIS_CODEGEN_PLATFORM", PlatformRegistry.global_platform_name)
69
+ model_name = os.environ.get("JARVIS_CODEGEN_MODEL")
65
70
  model = PlatformRegistry().get_global_platform_registry().create_platform(platform_name)
66
71
  if model_name:
67
72
  model.set_model_name(model_name)
@@ -71,10 +76,44 @@ class CodeEditTool:
71
76
  """判断代码库是否有未提交的文件"""
72
77
  os.chdir(root_dir)
73
78
  return os.system(f"git status | grep 'Changes not staged for commit'")
74
-
75
- def _get_key_info(self, file_path: str, content: str) -> Dict[str, Any]:
76
- """获取文件的关键信息"""
77
- model = self._new_model()
79
+
80
+ def _call_model_with_retry(self, model: BasePlatform, prompt: str, max_retries: int = 3, initial_delay: float = 1.0) -> Tuple[bool, str]:
81
+ """调用模型并支持重试
82
+
83
+ Args:
84
+ prompt: 提示词
85
+ max_retries: 最大重试次数
86
+ initial_delay: 初始延迟时间(秒)
87
+
88
+ Returns:
89
+ Tuple[bool, str]: (是否成功, 响应内容)
90
+ """
91
+ delay = initial_delay
92
+ for attempt in range(max_retries):
93
+ try:
94
+ response = model.chat(prompt)
95
+ return True, response
96
+ except Exception as e:
97
+ if attempt == max_retries - 1: # 最后一次尝试
98
+ PrettyOutput.print(f"调用模型失败: {str(e)}", OutputType.ERROR)
99
+ return False, str(e)
100
+
101
+ PrettyOutput.print(f"调用模型失败,{delay}秒后重试: {str(e)}", OutputType.WARNING)
102
+ time.sleep(delay)
103
+ delay *= 2 # 指数退避
104
+
105
+ def _get_key_info(self, file_path: str, content: str) -> Optional[Dict[str, Any]]:
106
+ """获取文件的关键信息
107
+
108
+ Args:
109
+ file_path: 文件路径
110
+ content: 文件内容
111
+
112
+ Returns:
113
+ Optional[Dict[str, Any]]: 文件信息,包含文件描述
114
+ """
115
+ model = self._new_model() # 创建新的模型实例
116
+
78
117
  prompt = f"""你是一个资深程序员,请根据文件内容,生成文件的关键信息,要求如下,除了代码,不要输出任何内容:
79
118
 
80
119
  1. 文件路径: {file_path}
@@ -82,22 +121,26 @@ class CodeEditTool:
82
121
  <CONTENT_START>
83
122
  {content}
84
123
  <CONTENT_END>
85
- 3. 关键信息: 请生成文件的关键信息,包括文件功能,符号列表(类型、名称、描述),使用标准的yaml格式描述,仅输出以下格式内容,如果目标文件不是代码文件,输出(无)
124
+ 3. 关键信息: 请生成文件的功能描述,使用标准的yaml格式描述,仅输出以下格式内容,如果目标文件不是代码文件,输出(无)
86
125
  <FILE_INFO_START>
87
- file_description: xxxx
88
- symbols:
89
- - name: xxxx
90
- description: xxxx
91
- type: xxxx
92
- - name: yyyy
93
- description: yyyy
94
- type: yyyy
126
+ file_description: 这个文件的主要功能和作用描述
95
127
  <FILE_INFO_END>
96
128
  """
97
- response = model.chat(prompt)
98
- model.delete_chat()
99
- response = response.replace("<FILE_INFO_START>", "").replace("<FILE_INFO_END>", "")
100
- return yaml.safe_load(response)
129
+ try:
130
+ response = model.chat(prompt)
131
+ model.delete_chat() # 删除会话历史
132
+
133
+ response = response.replace("<FILE_INFO_START>", "").replace("<FILE_INFO_END>", "")
134
+ return yaml.safe_load(response)
135
+ except Exception as e:
136
+ PrettyOutput.print(f"解析文件信息失败: {str(e)}", OutputType.ERROR)
137
+ return None
138
+ finally:
139
+ # 确保清理模型资源
140
+ try:
141
+ model.delete_chat()
142
+ except:
143
+ pass
101
144
 
102
145
  def _get_file_extensions(self, language: str) -> List[str]:
103
146
  """获取文件扩展名"""
@@ -106,15 +149,15 @@ symbols:
106
149
  def _get_file_md5(self, file_path: str) -> str:
107
150
  """获取文件MD5"""
108
151
  return hashlib.md5(open(file_path, "rb").read()).hexdigest()
109
-
152
+
110
153
  def _create_index_db(self):
111
154
  """创建索引数据库"""
112
155
  index_db_path = os.path.join(self.root_dir, ".index.db")
113
156
  if not os.path.exists(index_db_path):
114
157
  PrettyOutput.print("Index database does not exist, creating...", OutputType.INFO)
115
158
  index_db = sqlite3.connect(index_db_path)
116
- index_db.execute("CREATE TABLE files (file_path TEXT PRIMARY KEY, file_md5 TEXT, file_description TEXT)")
117
- index_db.execute("CREATE TABLE symbols (file_path TEXT, symbol_name TEXT, symbol_description TEXT, symbol_type TEXT)")
159
+ index_db.execute(
160
+ "CREATE TABLE files (file_path TEXT PRIMARY KEY, file_md5 TEXT, file_description TEXT)")
118
161
  index_db.commit()
119
162
  index_db.close()
120
163
  PrettyOutput.print("Index database created", OutputType.SUCCESS)
@@ -132,30 +175,31 @@ symbols:
132
175
  """根据文件MD5查找文件路径"""
133
176
  index_db = sqlite3.connect(index_db_path)
134
177
  cursor = index_db.cursor()
135
- cursor.execute("SELECT file_path FROM files WHERE file_md5 = ?", (file_md5,))
178
+ cursor.execute(
179
+ "SELECT file_path FROM files WHERE file_md5 = ?", (file_md5,))
136
180
  result = cursor.fetchone()
137
181
  index_db.close()
138
182
  return result[0] if result else None
139
-
183
+
140
184
  def _update_file_path(self, index_db_path: str, file_path: str, file_md5: str):
141
185
  """更新文件路径"""
142
186
  index_db = sqlite3.connect(index_db_path)
143
187
  cursor = index_db.cursor()
144
- cursor.execute("UPDATE files SET file_path = ? WHERE file_md5 = ?", (file_path, file_md5))
188
+ cursor.execute(
189
+ "UPDATE files SET file_path = ? WHERE file_md5 = ?", (file_path, file_md5))
145
190
  index_db.commit()
146
191
  index_db.close()
147
192
 
148
- def _insert_info(self, index_db_path: str, file_path: str, file_md5: str, file_description: str, symbols: List[Dict[str, Any]]):
193
+ def _insert_info(self, index_db_path: str, file_path: str, file_md5: str, file_description: str):
149
194
  """插入文件信息"""
150
195
  index_db = sqlite3.connect(index_db_path)
151
196
  cursor = index_db.cursor()
152
197
  cursor.execute("DELETE FROM files WHERE file_path = ?", (file_path,))
153
- cursor.execute("INSERT INTO files (file_path, file_md5, file_description) VALUES (?, ?, ?)", (file_path, file_md5, file_description))
154
- for symbol in symbols:
155
- cursor.execute("INSERT INTO symbols (file_path, symbol_name, symbol_description, symbol_type) VALUES (?, ?, ?, ?)", (file_path, symbol["name"], symbol["description"], symbol["type"]))
198
+ cursor.execute("INSERT INTO files (file_path, file_md5, file_description) VALUES (?, ?, ?)",
199
+ (file_path, file_md5, file_description))
156
200
  index_db.commit()
157
201
  index_db.close()
158
-
202
+
159
203
  def _index_project(self, language: str):
160
204
  """建立代码库索引"""
161
205
  # 1. 创建索引数据库,位于root_dir/.index.db
@@ -163,7 +207,7 @@ symbols:
163
207
  self.db_path = index_db_path
164
208
  if not os.path.exists(index_db_path):
165
209
  self._create_index_db()
166
-
210
+
167
211
  # 2. 遍历文件
168
212
  for root, dirs, files in os.walk(self.root_dir):
169
213
  for file in files:
@@ -173,192 +217,211 @@ symbols:
173
217
  file_md5 = self._get_file_md5(file_path)
174
218
 
175
219
  # 查找文件
176
- file_path_in_db = self._find_file_by_md5(self.db_path, file_md5)
220
+ file_path_in_db = self._find_file_by_md5(
221
+ self.db_path, file_md5)
177
222
  if file_path_in_db:
178
- PrettyOutput.print(f"File {file_path} is duplicate, skip", OutputType.INFO)
223
+ PrettyOutput.print(
224
+ f"File {file_path} is duplicate, skip", OutputType.INFO)
179
225
  if file_path_in_db != file_path:
180
- self._update_file_path(self.db_path, file_path, file_md5)
181
- PrettyOutput.print(f"File {file_path} is duplicate, update path to {file_path}", OutputType.INFO)
226
+ self._update_file_path(
227
+ self.db_path, file_path, file_md5)
228
+ PrettyOutput.print(
229
+ f"File {file_path} is duplicate, update path to {file_path}", OutputType.INFO)
182
230
  continue
183
231
 
184
232
  with open(file_path, "r", encoding="utf-8") as f:
185
233
  file_content = f.read()
186
234
  key_info = self._get_key_info(file_path, file_content)
187
235
  if not key_info:
188
- PrettyOutput.print(f"File {file_path} index failed", OutputType.INFO)
236
+ PrettyOutput.print(
237
+ f"File {file_path} index failed", OutputType.INFO)
189
238
  continue
190
- if "file_description" in key_info and "symbols" in key_info:
191
- self._insert_info(self.db_path, file_path, file_md5, key_info["file_description"], key_info["symbols"])
192
- PrettyOutput.print(f"File {file_path} is indexed", OutputType.INFO)
239
+ if "file_description" in key_info:
240
+ self._insert_info(
241
+ self.db_path, file_path, file_md5, key_info["file_description"])
242
+ PrettyOutput.print(
243
+ f"File {file_path} is indexed", OutputType.INFO)
193
244
  else:
194
- PrettyOutput.print(f"File {file_path} is not a code file, skip", OutputType.INFO)
245
+ PrettyOutput.print(
246
+ f"File {file_path} is not a code file, skip", OutputType.INFO)
195
247
  PrettyOutput.print("Index project finished", OutputType.INFO)
196
248
 
197
- def _find_related_files(self, feature: str) -> List[str]:
249
+ def _find_related_files(self, feature: str) -> List[Dict]:
198
250
  """根据需求描述,查找相关文件"""
199
- model = self._new_model()
200
-
201
- score = [[],[],[],[],[],[],[],[],[],[]]
202
-
203
- step = 50
251
+ score = [[], [], [], [], [], [], [], [], [], []]
252
+ step = 5
204
253
  offset = 0
205
-
206
- # 每次从数据库中提取50条记录,循环提取,直到提取完所有记录
254
+
207
255
  while True:
208
- index_db = sqlite3.connect(self.db_path)
209
- cursor = index_db.cursor()
210
- cursor.execute(f"SELECT file_path, file_description FROM files LIMIT {step} OFFSET {offset}")
211
- result = cursor.fetchall()
212
- index_db.close()
213
- if not result:
256
+ try:
257
+ index_db = sqlite3.connect(self.db_path)
258
+ cursor = index_db.cursor()
259
+ cursor.execute(f"SELECT file_path, file_description FROM files LIMIT {step} OFFSET {offset}")
260
+ result = cursor.fetchall()
261
+ index_db.close()
262
+
263
+ if not result:
264
+ break
265
+
266
+ offset += len(result)
267
+ prompt = "你是资深程序员,请根据需求描述,分析文件的相关性,文件列表如下:\n"
268
+ prompt += "<FILE_LIST_START>\n"
269
+ for i, file_path in enumerate(result):
270
+ prompt += f"""{i}. {file_path[0]} : {file_path[1]}\n"""
271
+ prompt += f"""需求描述: {feature}\n"""
272
+ prompt += "<FILE_LIST_END>\n"
273
+ prompt += "请根据需求描述和文件描述,分析文件的相关性,输出每个编号的相关性[0~9],仅输出以下格式内容(key为文件编号,value为相关性)\n"
274
+ prompt += "<FILE_RELATION_START>\n"
275
+ prompt += '''"0": 5'''
276
+ prompt += '''"1": 3'''
277
+ prompt += "<FILE_RELATION_END>\n"
278
+
279
+ success, response = self._call_model_with_retry(self._new_model(), prompt)
280
+ if not success:
281
+ continue
282
+
283
+ try:
284
+ response = response.replace("<FILE_RELATION_START>", "").replace("<FILE_RELATION_END>", "")
285
+ file_relation = yaml.safe_load(response)
286
+ if not file_relation:
287
+ PrettyOutput.print("响应格式错误", OutputType.WARNING)
288
+ continue
289
+
290
+ for file_id, relation in file_relation.items():
291
+ id = int(file_id)
292
+ relation = max(0, min(9, relation)) # 确保范围在0-9之间
293
+ score[relation].append({
294
+ "file_path": result[id][0],
295
+ "file_description": result[id][1]
296
+ })
297
+
298
+ except Exception as e:
299
+ PrettyOutput.print(f"处理文件关系失败: {str(e)}", OutputType.ERROR)
300
+ continue
301
+
302
+ except sqlite3.Error as e:
303
+ PrettyOutput.print(f"数据库操作失败: {str(e)}", OutputType.ERROR)
214
304
  break
215
- offset += len(result)
216
- prompt = "你是资深程序员,请根据需求描述,分析文件的相关性,文件列表如下:\n"
217
- prompt += "<FILE_LIST_START>\n"
218
- # 为文件生成编号,提供的信息有,文件序号,文件名,文件描述,并生成prompt,输出每个编号的相关性[1~10]
219
- for i, file_path in enumerate(result):
220
- prompt += f"""{i}. {file_path[0]} : {file_path[1]}\n"""
221
- prompt += f"""需求描述: {feature}\n"""
222
- prompt += "<FILE_LIST_END>\n"
223
- prompt += "请根据需求描述,分析文件的相关性,输出每个编号的相关性[0~9],仅输出以下格式内容(key为文件编号,value为相关性)\n"
224
- prompt += "<FILE_RELATION_START>\n"
225
- prompt += '''"0": 5'''
226
- prompt += '''"1": 3'''
227
- prompt += "<FILE_RELATION_END>\n"
228
- response = model.chat(prompt)
229
- model.delete_chat()
230
- response = response.replace("<FILE_RELATION_START>", "").replace("<FILE_RELATION_END>", "")
231
- file_relation = yaml.safe_load(response)
232
- if not file_relation:
233
- PrettyOutput.print("Response format error", OutputType.WARNING)
234
- continue
235
- for file_id, relation in file_relation.items():
236
- id = int(file_id)
237
- if relation>9:
238
- relation = 9
239
- if relation<0:
240
- relation = 0
241
- score[relation].append({"file_path": result[id][0], "file_description": result[id][1]})
242
-
305
+ except Exception as e:
306
+ PrettyOutput.print(f"查找相关文件失败: {str(e)}", OutputType.ERROR)
307
+ break
308
+
243
309
  files = []
244
310
  score.reverse()
245
311
  for i in score:
246
312
  files.extend(i)
247
- if len(files) >= 10:
313
+ if len(files) >= 10: # 先获取最多10个相关文件
248
314
  break
315
+
316
+ # 如果找到超过3个相关文件,再次筛选
317
+ if len(files) > 3:
318
+ prompt = "你是资深程序员,请从以下文件中挑选出与需求最相关的3个文件:\n"
319
+ prompt += "<FILE_LIST_START>\n"
320
+ for i, file in enumerate(files):
321
+ prompt += f"""{i}. {file["file_path"]} : {file["file_description"]}\n"""
322
+ prompt += f"""需求描述: {feature}\n"""
323
+ prompt += "<FILE_LIST_END>\n"
324
+ prompt += "请输出最相关的3个文件的编号,用逗号分隔,仅输出以下格式内容\n"
325
+ prompt += "<TOP3_START>\n"
326
+ prompt += "0,2,5"
327
+ prompt += "<TOP3_END>\n"
328
+
329
+ success, response = self._call_model_with_retry(self._new_model(), prompt)
330
+ if success:
331
+ try:
332
+ response = response.replace("<TOP3_START>", "").replace("<TOP3_END>", "")
333
+ top3_ids = [int(id.strip()) for id in response.split(",")]
334
+ files = [files[i] for i in top3_ids if i < len(files)]
335
+ except Exception as e:
336
+ PrettyOutput.print(f"解析TOP3文件失败: {str(e)}", OutputType.ERROR)
337
+ files = files[:3] # 如果解析失败,取前3个文件
338
+ else:
339
+ files = files[:3] # 如果模型调用失败,取前3个文件
340
+ else:
341
+ files = files[:3] # 如果文件数不足3个,取所有文件
342
+
249
343
  return files
344
+
345
+ def _remake_patch(self, prompt: str) -> List[str]:
346
+ success, response = self._call_model_with_retry(self.main_model, prompt, max_retries=5) # 增加重试次数
347
+ if not success:
348
+ return []
349
+
350
+ try:
351
+ patches = re.findall(r">>>>>>.*?<<<<<<", response, re.DOTALL)
352
+ return patches
353
+ except Exception as e:
354
+ PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.ERROR)
355
+ return []
250
356
 
251
- def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
252
- """执行代码修改
357
+ def _make_patch(self, related_files: List[Dict]) -> List[str]:
358
+ """生成修改方案"""
359
+ prompt = "你是一个资深程序员,请根据需求描述,修改文件内容,文件列表如下:\n"
360
+ prompt += "<FILE_RELATION_START>\n"
361
+ for i, file in enumerate(related_files):
362
+ prompt += f"""{i}. {file["file_path"]} : {file["file_description"]}\n"""
363
+ prompt += f"""文件内容: \n"""
364
+ prompt += f"<FILE_CONTENT_START>\n"
365
+ prompt += f'{file["file_content"]}\n'
366
+ prompt += f"<FILE_CONTENT_END>\n"
367
+ prompt += f"<FILE_RELATION_END>\n"
368
+ prompt += f"请根据需求描述,修改文件。\n"
369
+ prompt += f"需求描述: {self.feature}\n"
370
+ prompt += f"请输出以下格式内容(多段patch),注意缩进也是代码的一部分,不要输出任何其他内容\n"
371
+ prompt += f">>>>>> [文件路径1]\n"
372
+ prompt += f"[原文件内容]\n"
373
+ prompt += f"==========\n"
374
+ prompt += f"[修改后的文件内容]\n"
375
+ prompt += f"<<<<<<\n"
376
+ prompt += f"如果文件不存在,请创建新文件,不要包含原始内容,如下:\n"
377
+ prompt += f">>>>>> [文件路径1]\n"
378
+ prompt += f"==========\n"
379
+ prompt += f"[新文件内容]\n"
380
+ prompt += f"<<<<<<\n"
381
+ prompt += f"生成最小化的patch,原文件内容与修改后文件内容不要有大量重复内容。"
382
+ prompt += f"如果一个文件有多处需要修改,请生成多个patch,不要生成一个patch包含多处修改。"
383
+
384
+ success, response = self._call_model_with_retry(self.main_model, prompt, max_retries=5) # 增加重试次数
385
+ if not success:
386
+ return []
387
+
388
+ try:
389
+ patches = re.findall(r">>>>>>.*?<<<<<<", response, re.DOTALL)
390
+ return patches
391
+ except Exception as e:
392
+ PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.ERROR)
393
+ return []
394
+
395
+ def _apply_patch(self, related_files: List[Dict], patches: List[str]) -> Tuple[bool, str]:
396
+ """应用补丁
253
397
 
254
398
  Args:
255
- args: 包含操作参数的字典
256
- - feature: 要实现的功能描述
257
- - root_dir: 代码库根目录
258
- - language: 编程语言
399
+ related_files: 相关文件列表
400
+ patches: 补丁列表
259
401
 
260
402
  Returns:
261
- Dict[str, Any]: 包含执行结果的字典
262
- - success: 是否成功
263
- - changes: 修改内容
264
- - preview: 修改预览
265
- - results: 执行结果
403
+ Tuple[bool, str]: (是否成功, 错误信息)
266
404
  """
267
- self.feature = args["feature"]
268
- self.root_dir = args["root_dir"]
269
- self.language = args["language"]
270
-
271
- self.current_dir = os.getcwd()
272
-
273
- # 0. 判断语言是否支持
274
- if not self._get_file_extensions(self.language):
275
- PrettyOutput.print("Language not supported", OutputType.ERROR)
276
- return {"success": False, "changes": "", "preview": "", "results": ""}
277
-
278
- # 1. 判断代码库路径是否存在,如果不存在,创建
279
- if not os.path.exists(self.root_dir):
280
- PrettyOutput.print("Root directory does not exist, creating...", OutputType.INFO)
281
- os.makedirs(self.root_dir)
282
-
283
- # 2. 判断代码库是否是git仓库,如果不是,初始化git仓库
284
- if not os.path.exists(os.path.join(self.root_dir, ".git")):
285
- PrettyOutput.print("Git repository does not exist, initializing...", OutputType.INFO)
286
- os.chdir(self.root_dir)
287
- os.system(f"git init")
288
- # 2.1 添加所有的文件
289
- os.system(f"git add .")
290
- # 2.2 提交
291
- os.system(f"git commit -m 'Initial commit'")
292
- os.chdir(self.current_dir)
293
-
294
- # 3. 查看代码库是否有未提交的文件,如果有,提交一次
295
- if self._has_uncommitted_files(self.root_dir):
296
- os.chdir(self.root_dir)
297
- os.system(f"git add .")
298
- os.system(f"git commit -m 'commit before code edit'")
299
- os.chdir(self.current_dir)
300
-
301
- # 4. 开始建立代码库索引
302
- os.chdir(self.root_dir)
303
- self._index_project(self.language)
304
- os.chdir(self.current_dir)
305
-
306
- # 5. 根据索引和需求,查找相关文件
307
- related_files = self._find_related_files(self.feature)
308
- PrettyOutput.print(f"Related files: {related_files}", OutputType.INFO)
309
- for file in related_files:
310
- with open(file["file_path"], "r", encoding="utf-8") as f:
311
- file_content = f.read()
312
- file["file_content"] = file_content
313
-
314
- # 6. 根据相关文件,生成修改方案
315
- patches = self._make_patch(related_files)
316
- PrettyOutput.print(f"生成{len(patches)}个patch", OutputType.INFO)
317
-
318
- if len(patches) == 0:
319
- PrettyOutput.print("No patch generated, skip", OutputType.INFO)
320
- return {"success": False, "changes": "", "preview": "", "results": ""}
321
-
322
- # 7. 应用修改方案
323
- self._apply_patch(related_files, patches)
324
-
325
- # 8. 用户确认修改
326
- user_confirm = input("是否确认修改?(y/n)")
327
- if user_confirm == "y":
328
- PrettyOutput.print("修改确认成功,提交修改", OutputType.INFO)
329
- os.chdir(self.root_dir)
330
- os.system(f"git add .")
331
- os.system(f"git commit -m '{self.feature}'")
332
- os.chdir(self.current_dir)
333
- # 9. 重新建立代码库索引
334
- self._index_project(self.language)
335
- else:
336
- PrettyOutput.print("修改确认失败,取消修改", OutputType.INFO)
337
- os.chdir(self.root_dir)
338
- os.system(f"git reset --hard")
339
- os.chdir(self.current_dir)
340
-
341
- def _apply_patch(self, related_files, patches):
342
- # patch格式
343
- # >>>>>> [文件路径1]
344
- # [原文件内容]
345
- # [原文件内容]
346
- # ==========
347
- # [修改后的文件内容]
348
- # [修改后的文件内容]
349
- # <<<<<<
350
-
405
+ # 创建文件内容映射
351
406
  file_map = {file["file_path"]: file["file_content"] for file in related_files}
407
+ temp_map = file_map.copy() # 创建临时映射用于尝试应用
408
+
409
+ error_info = []
352
410
 
411
+ modified_files = set() # 记录修改的文件
412
+
413
+ # 尝试应用所有补丁
353
414
  for i, patch in enumerate(patches):
354
- PrettyOutput.print(f"Apply patch {i+1} of {len(patches)}", OutputType.INFO)
355
- patch_line = patch.split("\n")
415
+ PrettyOutput.print(f"正在应用补丁 {i+1}/{len(patches)}", OutputType.INFO)
416
+ patch_lines = patch.split("\n")
356
417
  file_name = ""
357
418
  old_code = []
358
419
  new_code = []
359
420
  old_code_flag = False
360
421
  new_code_flag = False
361
- for line in patch_line:
422
+
423
+ # 解析补丁内容
424
+ for line in patch_lines:
362
425
  if line.startswith(">>>>>>"):
363
426
  old_code_flag = True
364
427
  file_name = line.split(" ")[1]
@@ -371,60 +434,296 @@ symbols:
371
434
  old_code.append(line)
372
435
  elif new_code_flag:
373
436
  new_code.append(line)
374
- if file_name not in file_map:
375
- PrettyOutput.print(f"File {file_name} not found in related files, create it", OutputType.WARNING)
376
- with open(file_name, "w", encoding="utf-8") as f:
377
- f.write("\n".join(new_code))
378
- else:
379
- old_code = "\n".join(old_code)
380
- new_code = "\n".join(new_code)
381
- if old_code in file_map[file_name]:
382
- file_map[file_name] = file_map[file_name].replace(old_code, new_code)
383
- with open(file_name, "w", encoding="utf-8") as f:
384
- f.write(file_map[file_name])
385
- PrettyOutput.print(f"File {file_name} apply patch success", OutputType.SUCCESS)
386
- else:
387
- PrettyOutput.print(f"File {file_name} apply patch failed", OutputType.ERROR)
388
- PrettyOutput.print(f"Old code: \n{old_code}\n", OutputType.INFO)
389
- PrettyOutput.print(f"New code: \n{new_code}\n", OutputType.INFO)
390
- PrettyOutput.print("Apply patch finished", OutputType.INFO)
437
+
438
+ # 处理新文件的情况
439
+ if file_name not in temp_map:
440
+ PrettyOutput.print(f"创建新文件: {file_name}", OutputType.WARNING)
441
+ if old_code: # 如果是新文件但有原始内容,这是错误的
442
+ error_info.append(f"文件 {file_name} 不存在,但补丁包含原始内容")
443
+ return False, "\n".join(error_info)
444
+
445
+ temp_map[file_name] = "\n".join(new_code)
446
+ modified_files.add(file_name) # 记录新文件
447
+ continue
448
+
449
+ # 应用补丁到现有文件
450
+ old_content = "\n".join(old_code)
451
+ new_content = "\n".join(new_code)
452
+
453
+ if old_content not in temp_map[file_name]:
454
+ error_info.append(
455
+ f"补丁应用失败: {file_name}\n"
456
+ f"原因: 未找到要替换的代码\n"
457
+ f"期望找到的代码:\n{old_content}\n"
458
+ f"实际文件内容:\n{temp_map[file_name][:200]}..." # 只显示前200个字符
459
+ )
460
+ return False, "\n".join(error_info)
461
+
462
+ # 应用更改到临时映射
463
+ temp_map[file_name] = temp_map[file_name].replace(old_content, new_content)
464
+ modified_files.add(file_name) # 记录修改的文件
465
+
466
+ # 所有补丁都应用成功,更新实际文件
467
+ for file_path in modified_files:
468
+ try:
469
+ dir = os.path.dirname(file_path)
470
+ if dir and not os.path.exists(dir):
471
+ os.makedirs(dir, exist_ok=True)
472
+ PrettyOutput.print(f"更新文件: {file_path}", OutputType.INFO)
473
+ with open(file_path, "w", encoding="utf-8") as f:
474
+ f.write(temp_map[file_path])
475
+ PrettyOutput.print(f"成功更新文件: {file_path}", OutputType.SUCCESS)
476
+ except Exception as e:
477
+ error_info.append(f"写入文件失败 {file_path}: {str(e)}")
478
+ return False, "\n".join(error_info)
391
479
 
480
+ return True, ""
392
481
 
393
- def _make_patch(self, related_files):
482
+ def _save_edit_record(self, feature: str, patches: List[str]) -> None:
483
+ """保存代码修改记录
394
484
 
395
- prompt = "你是一个资深程序员,请根据需求描述,修改文件内容,生成最小化的patch,文件列表如下:\n"
396
- prompt += "<FILE_RELATION_START>\n"
397
- for i, file in enumerate(related_files):
398
- prompt += f"""{i}. {file["file_path"]} : {file["file_description"]}\n"""
399
- prompt += f"""文件内容: \n"""
400
- prompt += f"<FILE_CONTENT_START>\n"
401
- prompt += f"{file["file_content"]}\n"
402
- prompt += f"<FILE_CONTENT_END>\n"
403
- prompt += f"<FILE_RELATION_END>\n"
404
- prompt += f"请根据需求描述,修改文件。\n"
405
- prompt += f"需求描述: {self.feature}\n"
406
- prompt += f"请输出以下格式内容(多段patch),注意缩进也是代码的一部分,不要输出任何其他内容\n"
407
- prompt += f">>>>>> [文件路径1]\n"
408
- prompt += f"[原文件内容]\n"
409
- prompt += f"==========\n"
410
- prompt += f"[修改后的文件内容]\n"
411
- prompt += f"<<<<<<\n"
412
- prompt += f">>>>>> [文件路径2]\n"
413
- prompt += f"[原文件内容]\n"
414
- prompt += f"==========\n"
415
- prompt += f"[修改后的文件内容]\n"
416
- prompt += f"<<<<<<\n"
417
- response = self.main_model.chat(prompt)
418
- self.main_model.delete_chat()
419
- # 使用<FILE_EDIT_START>和<FILE_EDIT_END>提取所有的patch,可能除了patch
420
- patch = re.findall(r">>>>>>.*?<<<<<<", response, re.DOTALL)
421
- return patch
485
+ Args:
486
+ feature: 需求描述
487
+ patches: 补丁列表
488
+ """
489
+ # 创建记录目录
490
+ record_dir = os.path.join(self.root_dir, ".jarvis_code_edit")
491
+ os.makedirs(record_dir, exist_ok=True)
492
+
493
+ # 添加到 .gitignore
494
+ gitignore_path = os.path.join(self.root_dir, ".gitignore")
495
+ if os.path.exists(gitignore_path):
496
+ with open(gitignore_path, "r") as f:
497
+ if ".jarvis_code_edit" not in f.read():
498
+ with open(gitignore_path, "a") as f:
499
+ f.write("\n.jarvis_code_edit/\n")
500
+ else:
501
+ with open(gitignore_path, "w") as f:
502
+ f.write(".jarvis_code_edit/\n")
503
+
504
+ # 获取下一个序号
505
+ existing_records = [f for f in os.listdir(record_dir) if f.endswith('.yaml')]
506
+ next_num = 1
507
+ if existing_records:
508
+ last_num = max(int(f[:4]) for f in existing_records)
509
+ next_num = last_num + 1
510
+
511
+ # 创建记录文件
512
+ record = {
513
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
514
+ "feature": feature,
515
+ "patches": patches
516
+ }
517
+
518
+ record_path = os.path.join(record_dir, f"{next_num:04d}.yaml")
519
+ with open(record_path, "w", encoding="utf-8") as f:
520
+ yaml.safe_dump(record, f, allow_unicode=True)
521
+
522
+ PrettyOutput.print(f"已保存修改记录: {record_path}", OutputType.SUCCESS)
422
523
 
524
+ def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
525
+ """执行代码修改
423
526
 
424
- if __name__ == "__main__":
527
+ Args:
528
+ args: 包含操作参数的字典
529
+ - feature: 要实现的功能描述
530
+ - root_dir: 代码库根目录
531
+ - language: 编程语言
532
+
533
+ Returns:
534
+ Dict[str, Any]: 包含执行结果的字典
535
+ - success: 是否成功
536
+ - stdout: 标准输出信息
537
+ - stderr: 错误信息
538
+ - error: 错误对象(如果有)
539
+ """
540
+ try:
541
+ self.feature = args["feature"]
542
+ self.root_dir = args["root_dir"]
543
+ self.language = args["language"]
544
+ self.current_dir = os.getcwd()
545
+
546
+ # 0. 判断语言是否支持
547
+ if not self._get_file_extensions(self.language):
548
+ return {
549
+ "success": False,
550
+ "stdout": "",
551
+ "stderr": "不支持的编程语言",
552
+ "error": ValueError("不支持的编程语言")
553
+ }
554
+
555
+ # 1. 判断代码库路径是否存在,如果不存在,创建
556
+ if not os.path.exists(self.root_dir):
557
+ PrettyOutput.print(
558
+ "Root directory does not exist, creating...", OutputType.INFO)
559
+ os.makedirs(self.root_dir)
560
+
561
+ # 2. 判断代码库是否是git仓库,如果不是,初始化git仓库
562
+ if not os.path.exists(os.path.join(self.root_dir, ".git")):
563
+ PrettyOutput.print(
564
+ "Git repository does not exist, initializing...", OutputType.INFO)
565
+ os.chdir(self.root_dir)
566
+ os.system(f"git init")
567
+ # 2.1 添加所有的文件
568
+ os.system(f"git add .")
569
+ # 2.2 提交
570
+ os.system(f"git commit -m 'Initial commit'")
571
+ os.chdir(self.current_dir)
572
+
573
+ # 3. 查看代码库是否有未提交的文件,如果有,提交一次
574
+ if self._has_uncommitted_files(self.root_dir):
575
+ os.chdir(self.root_dir)
576
+ os.system(f"git add .")
577
+ os.system(f"git commit -m 'commit before code edit'")
578
+ os.chdir(self.current_dir)
579
+
580
+ # 4. 开始建立代码库索引
581
+ os.chdir(self.root_dir)
582
+ self._index_project(self.language)
583
+ os.chdir(self.current_dir)
584
+
585
+ # 5. 根据索引和需求,查找相关文件
586
+ related_files = self._find_related_files(self.feature)
587
+ for file in related_files:
588
+ PrettyOutput.print(f"Related file: {file['file_path']}", OutputType.INFO)
589
+ for file in related_files:
590
+ with open(file["file_path"], "r", encoding="utf-8") as f:
591
+ file_content = f.read()
592
+ file["file_content"] = file_content
593
+ patches = self._make_patch(related_files)
594
+ while True:
595
+ # 生成修改方案
596
+ PrettyOutput.print(f"生成{len(patches)}个补丁", OutputType.INFO)
597
+
598
+ if not patches:
599
+ self._save_edit_record(self.feature, patches)
600
+ return {
601
+ "success": False,
602
+ "stdout": "",
603
+ "stderr": "未生成补丁",
604
+ "error": ValueError("未生成补丁")
605
+ }
606
+
607
+ # 尝试应用补丁
608
+ success, error_info = self._apply_patch(related_files, patches)
609
+
610
+ if success:
611
+ # 用户确认修改
612
+ user_confirm = input("是否确认修改?(y/n)")
613
+ if user_confirm.lower() == "y":
614
+ PrettyOutput.print("修改确认成功,提交修改", OutputType.INFO)
615
+ os.chdir(self.root_dir)
616
+ os.system(f"git add .")
617
+ os.system(f"git commit -m '{self.feature}'")
618
+ os.chdir(self.current_dir)
619
+ # 保存修改记录
620
+ self._save_edit_record(self.feature, patches)
621
+ # 重新建立代码库索引
622
+ self._index_project(self.language)
623
+
624
+ return {
625
+ "success": True,
626
+ "stdout": f"已完成功能开发{self.feature}",
627
+ "stderr": "",
628
+ "error": None
629
+ }
630
+ else:
631
+ PrettyOutput.print("修改已取消,回退更改", OutputType.INFO)
632
+ os.chdir(self.root_dir)
633
+ os.system(f"git reset --hard") # 回退已修改的文件
634
+ os.system(f"git clean -df") # 删除新创建的文件和目录
635
+ os.chdir(self.current_dir)
636
+ return {
637
+ "success": False,
638
+ "stdout": "",
639
+ "stderr": "修改被用户取消,文件未发生任何变化",
640
+ "error": UserWarning("用户取消修改")
641
+ }
642
+ else:
643
+ # 补丁应用失败,让模型重新生成
644
+ PrettyOutput.print(f"补丁应用失败,请求重新生成: {error_info}", OutputType.WARNING)
645
+ retry_prompt = f"""补丁应用失败,请根据以下错误信息重新生成补丁:
646
+
647
+ 错误信息:
648
+ {error_info}
649
+
650
+ 请确保:
651
+ 1. 准确定位要修改的代码位置
652
+ 2. 正确处理代码缩进
653
+ 3. 考虑代码上下文
654
+ 4. 对新文件不要包含原始内容
655
+ """
656
+ patches = self._remake_patch(retry_prompt)
657
+ continue
658
+
659
+ except Exception as e:
660
+ return {
661
+ "success": False,
662
+ "stdout": "",
663
+ "stderr": f"执行失败: {str(e)}",
664
+ "error": e
665
+ }
666
+
667
+
668
+ def main():
669
+ """命令行入口"""
670
+ import argparse
671
+
672
+ load_env_from_file()
673
+
674
+ parser = argparse.ArgumentParser(description='代码修改工具')
675
+ parser.add_argument('-p', '--platform', help='AI平台名称', default=os.environ.get('JARVIS_PLATFORM'))
676
+ parser.add_argument('-m', '--model', help='模型名称', default=os.environ.get('JARVIS_CODEGEN_MODEL'))
677
+ parser.add_argument('-d', '--dir', help='项目根目录', required=True)
678
+ parser.add_argument('-l', '--language', help='编程语言', required=True)
679
+ args = parser.parse_args()
680
+
681
+ # 设置平台
682
+ if not args.platform:
683
+ print("错误: 未指定AI平台,请使用 -p 参数或设置 JARVIS_PLATFORM 环境变量")
684
+ return 1
685
+
686
+ PlatformRegistry.get_global_platform_registry().set_global_platform_name(args.platform)
687
+
688
+ # 设置模型
689
+ if args.model:
690
+ os.environ['JARVIS_CODEGEN_MODEL'] = args.model
691
+
425
692
  tool = CodeEditTool()
426
- tool.execute({
427
- "feature": "将排序数据修改为随机生成",
428
- "root_dir": "/tmp/test",
429
- "language": "c"
430
- })
693
+
694
+ # 循环处理需求
695
+ while True:
696
+ try:
697
+ # 获取需求
698
+ feature = get_multiline_input("请输入开发需求 (输入空行退出):")
699
+
700
+ if not feature or feature == "__interrupt__":
701
+ break
702
+
703
+ # 执行修改
704
+ result = tool.execute({
705
+ "feature": feature,
706
+ "root_dir": args.dir,
707
+ "language": args.language
708
+ })
709
+
710
+ # 显示结果
711
+ if result["success"]:
712
+ PrettyOutput.print(result["stdout"], OutputType.SUCCESS)
713
+ else:
714
+ if result["stderr"]:
715
+ PrettyOutput.print(result["stderr"], OutputType.ERROR)
716
+ if result["error"]:
717
+ PrettyOutput.print(f"错误类型: {type(result['error']).__name__}", OutputType.ERROR)
718
+
719
+ except KeyboardInterrupt:
720
+ print("\n用户中断执行")
721
+ break
722
+ except Exception as e:
723
+ PrettyOutput.print(f"执行出错: {str(e)}", OutputType.ERROR)
724
+ continue
725
+
726
+ return 0
727
+
728
+ if __name__ == "__main__":
729
+ exit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: jarvis-ai-assistant
3
- Version: 0.1.52
3
+ Version: 0.1.53
4
4
  Summary: Jarvis: An AI assistant that uses tools to interact with the system
5
5
  Home-page: https://github.com/skyfireitdiy/Jarvis
6
6
  Author: skyfire
@@ -1,26 +1,26 @@
1
- jarvis/__init__.py,sha256=6dWTVW9nsh1mr5gOTx4Eh4EMiqgFX_aOYYteSCYl0oY,50
1
+ jarvis/__init__.py,sha256=F8J3ApdoQ_vwNwqBBUnMD8AdpsLq6Q9p0Y7y0KiEtlo,50
2
2
  jarvis/agent.py,sha256=s8VebNef8BbKM6D9IDWWN-XenEXEL9KoYnDLGKyZY58,12347
3
3
  jarvis/main.py,sha256=VIqMYnqNrmVcnO5YC-Tc-ATVBt8IlqIcXHjZvHmyCFs,6272
4
4
  jarvis/utils.py,sha256=JlkuC9RtspXH2VWDmj9nR0vnb8ie1gIsKc4vC7WRco8,7321
5
5
  jarvis/models/__init__.py,sha256=mrOt67nselz_H1gX9wdAO4y2DY5WPXzABqJbr5Des8k,63
6
- jarvis/models/ai8.py,sha256=9i7n_-TPbvq0AaRILs9ERQ7Vy5tDyoibXkiPsJvwQio,12520
6
+ jarvis/models/ai8.py,sha256=cJmOHgScU0tgkkYJQUrHfAFnZmDc7aFc0tS90uPJ_jw,12595
7
7
  jarvis/models/base.py,sha256=eeNJJbv9ikPVTtV_E7mgW8LZzVgjQ-OzxlHF6slYrHw,1237
8
8
  jarvis/models/kimi.py,sha256=N0bPQ1ugx0RwjR96jLchmOPvCmws-fXyA0mnOAdo2k4,17161
9
- jarvis/models/openai.py,sha256=pB7AaZuorHlmudTPaUnEbFOyl51Fy6uhU9KQBm98Ov8,4156
10
- jarvis/models/oyi.py,sha256=4_sppGOLS3rucBHinhzjLhSh_6rJKIgLu9h6trHIOGM,14900
9
+ jarvis/models/openai.py,sha256=pbbZKFV-fEGSVMTomT2WFP0lKQtodNkWZkLwCB7JAeA,4231
10
+ jarvis/models/oyi.py,sha256=fXR92NzrSOf40pagIrtJ1ughPlC9Qz7aelQy_ISzexM,14975
11
11
  jarvis/models/registry.py,sha256=iVBjN9ImEvGHcz8WR-z8pPMJQZI907o_nccVOFANhak,7951
12
12
  jarvis/tools/__init__.py,sha256=Kj1bKj34lwRDKMKHLOrLyQElf2lHbqA2tDgP359eaDo,71
13
13
  jarvis/tools/base.py,sha256=EGRGbdfbLXDLwtyoWdvp9rlxNX7bzc20t0Vc2VkwIEY,652
14
- jarvis/tools/code_edit.py,sha256=BfPPcPNuQWTPuOkl7TlIjrcFXpwPbWZpIk66crQdILA,18873
14
+ jarvis/tools/code_edit.py,sha256=oMoggVC66Ve_nsQMJiCP00GdH8QzfiLTQnZF9TKa0Vk,30990
15
15
  jarvis/tools/file_ops.py,sha256=h8g0eT9UvlJf4kt0DLXvdSsjcPj7x19lxWdDApeDfpg,3842
16
16
  jarvis/tools/generator.py,sha256=vVP3eN5cCDpRXf_fn0skETkPXAW1XZFWx9pt2_ahK48,5999
17
17
  jarvis/tools/methodology.py,sha256=G3cOaHTMujGZBhDLhQEqyCV2NISizO3MXRuho1KfI6Y,5223
18
18
  jarvis/tools/registry.py,sha256=NbH7A4A2lyN2IoyZGFwa5Ghed2dpzbJWCAd1Dg95WBI,7183
19
19
  jarvis/tools/shell.py,sha256=UPKshPyOaUwTngresUw-ot1jHjQIb4wCY5nkJqa38lU,2520
20
20
  jarvis/tools/sub_agent.py,sha256=rEtAmSVY2ZjFOZEKr5m5wpACOQIiM9Zr_3dT92FhXYU,2621
21
- jarvis_ai_assistant-0.1.52.dist-info/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
22
- jarvis_ai_assistant-0.1.52.dist-info/METADATA,sha256=gb4uoX5ywDb-uEgMmltUt6Omc2fyZ_zUbz4auhktAAU,10015
23
- jarvis_ai_assistant-0.1.52.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
24
- jarvis_ai_assistant-0.1.52.dist-info/entry_points.txt,sha256=iKu7OMfew9dtfGhW71gIMTg4wvafuPqKb4wyQOnMAGU,44
25
- jarvis_ai_assistant-0.1.52.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
26
- jarvis_ai_assistant-0.1.52.dist-info/RECORD,,
21
+ jarvis_ai_assistant-0.1.53.dist-info/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
22
+ jarvis_ai_assistant-0.1.53.dist-info/METADATA,sha256=B0f_N8wLMD1gT9x6qJkYWn4CgKX1o7NymKAPiMkuzCo,10015
23
+ jarvis_ai_assistant-0.1.53.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
24
+ jarvis_ai_assistant-0.1.53.dist-info/entry_points.txt,sha256=lXpI7P6S17pGqG8xtl3LRC8xG0PsAM4I92UqD2VIQX8,77
25
+ jarvis_ai_assistant-0.1.53.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
26
+ jarvis_ai_assistant-0.1.53.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
+ ce = jarvis.tools.code_edit:main
2
3
  jarvis = jarvis.main:main