devlake-mcp 0.4.1__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.
- devlake_mcp/__init__.py +7 -0
- devlake_mcp/__main__.py +10 -0
- devlake_mcp/cli.py +794 -0
- devlake_mcp/client.py +474 -0
- devlake_mcp/compat.py +165 -0
- devlake_mcp/config.py +204 -0
- devlake_mcp/constants.py +161 -0
- devlake_mcp/enums.py +58 -0
- devlake_mcp/generation_manager.py +296 -0
- devlake_mcp/git_utils.py +489 -0
- devlake_mcp/hooks/__init__.py +49 -0
- devlake_mcp/hooks/hook_utils.py +246 -0
- devlake_mcp/hooks/post_tool_use.py +325 -0
- devlake_mcp/hooks/pre_tool_use.py +110 -0
- devlake_mcp/hooks/record_session.py +183 -0
- devlake_mcp/hooks/session_start.py +81 -0
- devlake_mcp/hooks/stop.py +275 -0
- devlake_mcp/hooks/transcript_utils.py +547 -0
- devlake_mcp/hooks/user_prompt_submit.py +204 -0
- devlake_mcp/logging_config.py +202 -0
- devlake_mcp/retry_queue.py +556 -0
- devlake_mcp/server.py +664 -0
- devlake_mcp/session_manager.py +444 -0
- devlake_mcp/utils.py +225 -0
- devlake_mcp/version_utils.py +174 -0
- devlake_mcp-0.4.1.dist-info/METADATA +541 -0
- devlake_mcp-0.4.1.dist-info/RECORD +31 -0
- devlake_mcp-0.4.1.dist-info/WHEEL +5 -0
- devlake_mcp-0.4.1.dist-info/entry_points.txt +3 -0
- devlake_mcp-0.4.1.dist-info/licenses/LICENSE +21 -0
- devlake_mcp-0.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Generation 生命周期管理模块
|
|
5
|
+
|
|
6
|
+
功能:
|
|
7
|
+
1. 管理 generation_id 的生命周期(创建、获取、结束)
|
|
8
|
+
2. 支持跨 hook 的 generation 追踪
|
|
9
|
+
3. 为每次 AI 交互分配唯一标识
|
|
10
|
+
|
|
11
|
+
设计:
|
|
12
|
+
- generation_id: 一次完整 AI 交互的唯一标识(UUID)
|
|
13
|
+
- 状态文件: ~/.devlake/generation_state.json
|
|
14
|
+
- 与 session_id 关联
|
|
15
|
+
|
|
16
|
+
使用方式:
|
|
17
|
+
from devlake_mcp.generation_manager import start_generation, get_current_generation_id, end_generation
|
|
18
|
+
|
|
19
|
+
# 在 UserPromptSubmit 中创建
|
|
20
|
+
generation_id = start_generation(session_id)
|
|
21
|
+
|
|
22
|
+
# 在 PostToolUse 中获取
|
|
23
|
+
generation_id = get_current_generation_id(session_id)
|
|
24
|
+
|
|
25
|
+
# 在 Stop 中结束
|
|
26
|
+
end_generation(session_id)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
import uuid
|
|
32
|
+
import os
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
from typing import Optional, Dict, Union
|
|
36
|
+
|
|
37
|
+
from .utils import get_data_dir
|
|
38
|
+
from .enums import IDEType
|
|
39
|
+
|
|
40
|
+
# 配置日志
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ============================================================================
|
|
45
|
+
# 辅助函数
|
|
46
|
+
# ============================================================================
|
|
47
|
+
|
|
48
|
+
def _get_current_pid() -> int:
|
|
49
|
+
"""获取当前进程 PID"""
|
|
50
|
+
return os.getpid()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_generation_state_file(ide_type: IDEType) -> Path:
|
|
54
|
+
"""
|
|
55
|
+
获取当前进程的 generation 状态文件路径(进程级隔离)
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
ide_type: IDE 类型枚举
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
状态文件路径
|
|
62
|
+
|
|
63
|
+
格式: ~/.devlake/generation_<ide_type>_<pid>.json
|
|
64
|
+
例如: ~/.devlake/generation_claude_code_12345.json
|
|
65
|
+
|
|
66
|
+
说明:
|
|
67
|
+
使用进程级隔离 + 持久化存储确保:
|
|
68
|
+
1. 不同 IDE 类型(Claude Code/Cursor)互不干扰
|
|
69
|
+
2. 同一 IDE 的多个实例互不干扰
|
|
70
|
+
3. 无需文件锁,每个进程有独立状态文件
|
|
71
|
+
4. 状态文件不会被系统自动清理(使用 ~/.devlake 而非 /tmp)
|
|
72
|
+
"""
|
|
73
|
+
pid = _get_current_pid()
|
|
74
|
+
ide_type_str = ide_type.value
|
|
75
|
+
filename = f'generation_{ide_type_str}_{pid}.json'
|
|
76
|
+
return get_data_dir(persistent=True) / filename
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ============================================================================
|
|
80
|
+
# 状态文件管理(私有函数)
|
|
81
|
+
# ============================================================================
|
|
82
|
+
|
|
83
|
+
def _read_generation_state(ide_type: IDEType) -> Optional[Dict]:
|
|
84
|
+
"""
|
|
85
|
+
从状态文件读取 generation 信息
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
ide_type: IDE 类型枚举
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Generation 状态字典,如果文件不存在或损坏返回 None
|
|
92
|
+
|
|
93
|
+
状态格式:
|
|
94
|
+
{
|
|
95
|
+
"session_id": "abc-123",
|
|
96
|
+
"generation_id": "gen-uuid-456",
|
|
97
|
+
"started_at": "2025-01-08T10:00:00"
|
|
98
|
+
}
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
state_file = _get_generation_state_file(ide_type)
|
|
102
|
+
if state_file.exists():
|
|
103
|
+
with open(state_file, 'r', encoding='utf-8') as f:
|
|
104
|
+
return json.load(f)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning(f'读取 generation 状态文件失败: {e}')
|
|
107
|
+
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _write_generation_state(session_id: str, generation_id: str, ide_type: IDEType):
|
|
112
|
+
"""
|
|
113
|
+
写入 generation 状态到文件
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
session_id: 会话 ID
|
|
117
|
+
generation_id: Generation ID(UUID)
|
|
118
|
+
ide_type: IDE 类型枚举
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
state = {
|
|
122
|
+
'session_id': session_id,
|
|
123
|
+
'generation_id': generation_id,
|
|
124
|
+
'started_at': datetime.now().isoformat()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
state_file = _get_generation_state_file(ide_type)
|
|
128
|
+
|
|
129
|
+
# 确保目录存在
|
|
130
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
|
|
132
|
+
with open(state_file, 'w', encoding='utf-8') as f:
|
|
133
|
+
json.dump(state, f, ensure_ascii=False, indent=2)
|
|
134
|
+
|
|
135
|
+
logger.debug(f'Generation 状态已保存: session={session_id}, generation={generation_id}, 文件: {state_file.name}')
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f'保存 generation 状态失败: {e}')
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _clear_generation_state(ide_type: IDEType):
|
|
141
|
+
"""
|
|
142
|
+
清空 generation 状态文件
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
ide_type: IDE 类型枚举
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
state_file = _get_generation_state_file(ide_type)
|
|
149
|
+
if state_file.exists():
|
|
150
|
+
state_file.unlink()
|
|
151
|
+
logger.debug(f'Generation 状态文件已清空: {state_file.name}')
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.warning(f'清空 generation 状态文件失败: {e}')
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ============================================================================
|
|
157
|
+
# 公开 API
|
|
158
|
+
# ============================================================================
|
|
159
|
+
|
|
160
|
+
def start_generation(session_id: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE) -> str:
|
|
161
|
+
"""
|
|
162
|
+
开始新的 generation(生成并保存 generation_id)
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
session_id: 会话 ID
|
|
166
|
+
ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
新生成的 generation_id(UUID 字符串)
|
|
170
|
+
|
|
171
|
+
用途:
|
|
172
|
+
- 在 UserPromptSubmit Hook 中调用
|
|
173
|
+
- 为每次用户输入分配唯一的 generation_id
|
|
174
|
+
- 后续所有操作(工具调用、文件变更、响应)都关联到该 generation_id
|
|
175
|
+
|
|
176
|
+
示例:
|
|
177
|
+
generation_id = start_generation("session-123", ide_type=IDEType.CLAUDE_CODE)
|
|
178
|
+
# generation_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
179
|
+
"""
|
|
180
|
+
if not session_id:
|
|
181
|
+
logger.warning('session_id 为空,无法创建 generation')
|
|
182
|
+
return ''
|
|
183
|
+
|
|
184
|
+
# 转换为枚举类型(如果是字符串)
|
|
185
|
+
ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
|
|
186
|
+
|
|
187
|
+
# 生成 UUID 作为 generation_id
|
|
188
|
+
generation_id = str(uuid.uuid4())
|
|
189
|
+
|
|
190
|
+
# 保存到状态文件(进程级)
|
|
191
|
+
_write_generation_state(session_id, generation_id, ide_type_enum)
|
|
192
|
+
|
|
193
|
+
logger.info(f'新 generation 已创建: session={session_id}, generation={generation_id} ({ide_type_enum.value})')
|
|
194
|
+
|
|
195
|
+
return generation_id
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_current_generation_id(session_id: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE) -> Optional[str]:
|
|
199
|
+
"""
|
|
200
|
+
获取当前 session 的活跃 generation_id
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
session_id: 会话 ID
|
|
204
|
+
ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
当前的 generation_id,如果不存在返回 None
|
|
208
|
+
|
|
209
|
+
用途:
|
|
210
|
+
- 在 PreToolUse/PostToolUse 中获取当前 generation_id
|
|
211
|
+
- 在 Stop Hook 中获取 generation_id 以更新 prompt
|
|
212
|
+
- 关联文件变更到具体的 prompt
|
|
213
|
+
|
|
214
|
+
示例:
|
|
215
|
+
generation_id = get_current_generation_id("session-123", ide_type=IDEType.CLAUDE_CODE)
|
|
216
|
+
if generation_id:
|
|
217
|
+
# 使用 generation_id 关联数据
|
|
218
|
+
...
|
|
219
|
+
"""
|
|
220
|
+
if not session_id:
|
|
221
|
+
logger.debug('session_id 为空,无法获取 generation_id')
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
# 转换为枚举类型(如果是字符串)
|
|
225
|
+
ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
|
|
226
|
+
|
|
227
|
+
state = _read_generation_state(ide_type_enum)
|
|
228
|
+
|
|
229
|
+
# 验证 session_id 是否匹配
|
|
230
|
+
if state and state.get('session_id') == session_id:
|
|
231
|
+
generation_id = state.get('generation_id')
|
|
232
|
+
logger.debug(f'获取到 generation_id: {generation_id} (session: {session_id}, ide: {ide_type_enum.value})')
|
|
233
|
+
return generation_id
|
|
234
|
+
|
|
235
|
+
logger.debug(f'未找到匹配的 generation_id (session: {session_id}, ide: {ide_type_enum.value})')
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def end_generation(session_id: str, ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE):
|
|
240
|
+
"""
|
|
241
|
+
结束当前 generation(清空状态)
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
session_id: 会话 ID
|
|
245
|
+
ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
|
|
246
|
+
|
|
247
|
+
用途:
|
|
248
|
+
- 在 Stop Hook 中调用
|
|
249
|
+
- 标记当前 generation 已完成
|
|
250
|
+
- 清理状态文件,为下一次 generation 做准备
|
|
251
|
+
|
|
252
|
+
注意:
|
|
253
|
+
- 只有当 session_id 匹配时才清空状态
|
|
254
|
+
- 如果 session_id 不匹配,不会清空(避免误删其他会话的状态)
|
|
255
|
+
|
|
256
|
+
示例:
|
|
257
|
+
end_generation("session-123", ide_type=IDEType.CLAUDE_CODE)
|
|
258
|
+
"""
|
|
259
|
+
if not session_id:
|
|
260
|
+
logger.warning('session_id 为空,无法结束 generation')
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
# 转换为枚举类型(如果是字符串)
|
|
264
|
+
ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
|
|
265
|
+
|
|
266
|
+
state = _read_generation_state(ide_type_enum)
|
|
267
|
+
|
|
268
|
+
# 验证 session_id 是否匹配
|
|
269
|
+
if state and state.get('session_id') == session_id:
|
|
270
|
+
generation_id = state.get('generation_id')
|
|
271
|
+
_clear_generation_state(ide_type_enum)
|
|
272
|
+
logger.info(f'Generation 已结束: session={session_id}, generation={generation_id} ({ide_type_enum.value})')
|
|
273
|
+
else:
|
|
274
|
+
logger.debug(f'session_id 不匹配或状态不存在,跳过清理 (session: {session_id}, ide: {ide_type_enum.value})')
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def clear_generation(ide_type: Union[IDEType, str] = IDEType.CLAUDE_CODE):
|
|
278
|
+
"""
|
|
279
|
+
清空 generation 状态(强制清理)
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
ide_type: IDE 类型 (IDEType 枚举或字符串,默认 Claude Code)
|
|
283
|
+
|
|
284
|
+
用途:
|
|
285
|
+
- 手动清理状态(测试或调试)
|
|
286
|
+
- 重置 generation 追踪
|
|
287
|
+
|
|
288
|
+
注意:
|
|
289
|
+
- 不验证 session_id,直接清空
|
|
290
|
+
- 慎用,可能影响正在进行的交互
|
|
291
|
+
"""
|
|
292
|
+
# 转换为枚举类型(如果是字符串)
|
|
293
|
+
ide_type_enum = IDEType.from_string(ide_type) if isinstance(ide_type, str) else ide_type
|
|
294
|
+
|
|
295
|
+
_clear_generation_state(ide_type_enum)
|
|
296
|
+
logger.info(f'Generation 状态已强制清空 ({ide_type_enum.value})')
|