dev-recall 0.2.0__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.
- dev_recall-0.2.0.dist-info/METADATA +281 -0
- dev_recall-0.2.0.dist-info/RECORD +34 -0
- dev_recall-0.2.0.dist-info/WHEEL +5 -0
- dev_recall-0.2.0.dist-info/entry_points.txt +2 -0
- dev_recall-0.2.0.dist-info/top_level.txt +1 -0
- recall/__init__.py +3 -0
- recall/_hooks.py +211 -0
- recall/cli.py +1032 -0
- recall/collectors/__init__.py +1 -0
- recall/collectors/ai_chat.py +644 -0
- recall/collectors/containers.py +164 -0
- recall/collectors/git.py +540 -0
- recall/collectors/linux_process.py +230 -0
- recall/collectors/linux_session.py +229 -0
- recall/collectors/linux_window.py +199 -0
- recall/collectors/shell.py +300 -0
- recall/collectors/vscode.py +175 -0
- recall/config.py +257 -0
- recall/daemon.py +466 -0
- recall/daemon_main.py +25 -0
- recall/mcp_server.py +290 -0
- recall/models.py +225 -0
- recall/processor/__init__.py +1 -0
- recall/processor/embedder.py +213 -0
- recall/processor/enricher.py +213 -0
- recall/processor/session.py +142 -0
- recall/query/__init__.py +1 -0
- recall/query/context.py +130 -0
- recall/query/llm.py +85 -0
- recall/query/retriever.py +147 -0
- recall/query/timeparser.py +188 -0
- recall/storage/__init__.py +1 -0
- recall/storage/db.py +528 -0
- recall/storage/vectors.py +166 -0
recall/mcp_server.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""MCP server — exposes Recall tools via the Model Context Protocol (stdio)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
mcp = FastMCP("dev-recall")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Tool: recall
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@mcp.tool()
|
|
22
|
+
def recall(query: str, days: int = 7, top_k: int = 10) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Search developer activity history by natural language query.
|
|
25
|
+
|
|
26
|
+
Returns relevant events: terminal commands, git commits, file edits, AI chats.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
query: Natural language query about past work
|
|
30
|
+
days: How many days back to search (default 7)
|
|
31
|
+
top_k: Number of results to return (default 10)
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
from recall.config import load_config
|
|
35
|
+
from recall.storage.db import DB
|
|
36
|
+
from recall.storage.vectors import VectorStore
|
|
37
|
+
from recall.processor.embedder import EmbedderQueue
|
|
38
|
+
from recall.query.retriever import Retriever
|
|
39
|
+
from recall.query.timeparser import to_iso
|
|
40
|
+
|
|
41
|
+
config = load_config()
|
|
42
|
+
if not config.db_path.exists():
|
|
43
|
+
return "Recall database not found. Run: recall init"
|
|
44
|
+
|
|
45
|
+
now = datetime.now(timezone.utc)
|
|
46
|
+
from datetime import timedelta
|
|
47
|
+
start = now - timedelta(days=days)
|
|
48
|
+
date_range = (start, now)
|
|
49
|
+
|
|
50
|
+
db = DB(config.db_path)
|
|
51
|
+
vectors = VectorStore.from_file(config.faiss_path, dim=config.embedding_dim)
|
|
52
|
+
embedder = EmbedderQueue(db=db, vectors=vectors, model_name=config.embedding_model)
|
|
53
|
+
retriever = Retriever(db=db, vectors=vectors, embedder=embedder)
|
|
54
|
+
|
|
55
|
+
events = retriever.search(query, top_k=top_k, date_range=date_range)
|
|
56
|
+
db.close()
|
|
57
|
+
|
|
58
|
+
if not events:
|
|
59
|
+
return f"No activity found matching '{query}' in the last {days} days."
|
|
60
|
+
|
|
61
|
+
lines = [f"Found {len(events)} relevant activities:\n"]
|
|
62
|
+
for i, event in enumerate(events, 1):
|
|
63
|
+
ts = _fmt_ts(event.timestamp)
|
|
64
|
+
lines.append(f"{i}. [{ts}] {event.content}")
|
|
65
|
+
|
|
66
|
+
return "\n".join(lines)
|
|
67
|
+
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
logger.exception("recall tool error")
|
|
70
|
+
return f"Error: {exc}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Tool: today_summary
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@mcp.tool()
|
|
79
|
+
def today_summary() -> str:
|
|
80
|
+
"""
|
|
81
|
+
Get a summary of what the developer worked on today.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
from recall.config import load_config
|
|
85
|
+
from recall.storage.db import DB
|
|
86
|
+
from recall.query.llm import is_available, ask as llm_ask, DevMemLLMError, configure
|
|
87
|
+
from recall.query.context import build_prompt_summary
|
|
88
|
+
|
|
89
|
+
config = load_config()
|
|
90
|
+
if not config.db_path.exists():
|
|
91
|
+
return "Recall database not found. Run: recall init"
|
|
92
|
+
|
|
93
|
+
configure(model=config.llm_model)
|
|
94
|
+
db = DB(config.db_path)
|
|
95
|
+
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
96
|
+
events = db.get_events_by_date(date_str)
|
|
97
|
+
|
|
98
|
+
if not events:
|
|
99
|
+
db.close()
|
|
100
|
+
return "No activity recorded today yet."
|
|
101
|
+
|
|
102
|
+
# Check cached summary
|
|
103
|
+
cached = db.get_daily_summary(date_str)
|
|
104
|
+
if cached and cached.get("summary"):
|
|
105
|
+
db.close()
|
|
106
|
+
return cached["summary"]
|
|
107
|
+
|
|
108
|
+
if not is_available():
|
|
109
|
+
db.close()
|
|
110
|
+
lines = [f"Today's activity ({date_str}, {len(events)} events):\n"]
|
|
111
|
+
for e in events[:20]:
|
|
112
|
+
lines.append(f"• {e.content}")
|
|
113
|
+
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
messages = build_prompt_summary(date_str, events)
|
|
116
|
+
summary = llm_ask(messages)
|
|
117
|
+
db.close()
|
|
118
|
+
return summary
|
|
119
|
+
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
logger.exception("today_summary tool error")
|
|
122
|
+
return f"Error: {exc}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Tool: recent_repos
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@mcp.tool()
|
|
131
|
+
def recent_repos(days: int = 7) -> str:
|
|
132
|
+
"""
|
|
133
|
+
List repositories the developer was active in recently.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
days: How many days back to look (default 7)
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
from recall.config import load_config
|
|
140
|
+
from recall.storage.db import DB
|
|
141
|
+
from datetime import timedelta
|
|
142
|
+
|
|
143
|
+
config = load_config()
|
|
144
|
+
if not config.db_path.exists():
|
|
145
|
+
return "Recall database not found. Run: recall init"
|
|
146
|
+
|
|
147
|
+
db = DB(config.db_path)
|
|
148
|
+
now = datetime.now(timezone.utc)
|
|
149
|
+
start = (now - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
150
|
+
end = now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
151
|
+
|
|
152
|
+
events = db.get_events_by_date_range(start, end)
|
|
153
|
+
db.close()
|
|
154
|
+
|
|
155
|
+
repo_activity: dict[str, int] = {}
|
|
156
|
+
for e in events:
|
|
157
|
+
if e.repo_name:
|
|
158
|
+
repo_activity[e.repo_name] = repo_activity.get(e.repo_name, 0) + 1
|
|
159
|
+
|
|
160
|
+
if not repo_activity:
|
|
161
|
+
return f"No repository activity in the last {days} days."
|
|
162
|
+
|
|
163
|
+
lines = [f"Active repositories (last {days} days):\n"]
|
|
164
|
+
for repo, count in sorted(repo_activity.items(), key=lambda x: -x[1]):
|
|
165
|
+
lines.append(f"• {repo} ({count} events)")
|
|
166
|
+
return "\n".join(lines)
|
|
167
|
+
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
logger.exception("recent_repos tool error")
|
|
170
|
+
return f"Error: {exc}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Tool: find_command
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@mcp.tool()
|
|
179
|
+
def find_command(description: str, repo: Optional[str] = None) -> str:
|
|
180
|
+
"""
|
|
181
|
+
Find a terminal command previously run, by describing what it does.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
description: Natural language description of what the command does
|
|
185
|
+
repo: Optional: limit search to a specific repo name
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
from recall.config import load_config
|
|
189
|
+
from recall.storage.db import DB
|
|
190
|
+
from recall.storage.vectors import VectorStore
|
|
191
|
+
from recall.processor.embedder import EmbedderQueue
|
|
192
|
+
from recall.query.retriever import Retriever
|
|
193
|
+
from recall.models import EventType
|
|
194
|
+
|
|
195
|
+
config = load_config()
|
|
196
|
+
if not config.db_path.exists():
|
|
197
|
+
return "Recall database not found. Run: recall init"
|
|
198
|
+
|
|
199
|
+
db = DB(config.db_path)
|
|
200
|
+
vectors = VectorStore.from_file(config.faiss_path, dim=config.embedding_dim)
|
|
201
|
+
embedder = EmbedderQueue(db=db, vectors=vectors, model_name=config.embedding_model)
|
|
202
|
+
retriever = Retriever(db=db, vectors=vectors, embedder=embedder)
|
|
203
|
+
|
|
204
|
+
events = retriever.search(
|
|
205
|
+
description,
|
|
206
|
+
top_k=10,
|
|
207
|
+
event_types=[EventType.TERMINAL_CMD],
|
|
208
|
+
repo_name=repo,
|
|
209
|
+
)
|
|
210
|
+
db.close()
|
|
211
|
+
|
|
212
|
+
if not events:
|
|
213
|
+
return f"No matching commands found for: '{description}'"
|
|
214
|
+
|
|
215
|
+
lines = [f"Commands matching '{description}':\n"]
|
|
216
|
+
for i, event in enumerate(events, 1):
|
|
217
|
+
ts = _fmt_ts(event.timestamp)
|
|
218
|
+
cmd = event.raw_data.get("cmd", event.content)
|
|
219
|
+
lines.append(f"{i}. [{ts}] {cmd}")
|
|
220
|
+
|
|
221
|
+
return "\n".join(lines)
|
|
222
|
+
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
logger.exception("find_command tool error")
|
|
225
|
+
return f"Error: {exc}"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Tool: timeline
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@mcp.tool()
|
|
234
|
+
def timeline(date: Optional[str] = None) -> str:
|
|
235
|
+
"""
|
|
236
|
+
Get chronological activity log for a specific date.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
date: Date in YYYY-MM-DD format. Defaults to today.
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
from recall.config import load_config
|
|
243
|
+
from recall.storage.db import DB
|
|
244
|
+
|
|
245
|
+
config = load_config()
|
|
246
|
+
if not config.db_path.exists():
|
|
247
|
+
return "Recall database not found. Run: recall init"
|
|
248
|
+
|
|
249
|
+
db = DB(config.db_path)
|
|
250
|
+
date_str = date or datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
251
|
+
events = db.get_events_by_date(date_str)
|
|
252
|
+
db.close()
|
|
253
|
+
|
|
254
|
+
if not events:
|
|
255
|
+
return f"No activity recorded on {date_str}."
|
|
256
|
+
|
|
257
|
+
lines = [f"Timeline for {date_str} ({len(events)} events):\n"]
|
|
258
|
+
for event in events:
|
|
259
|
+
ts = _fmt_ts(event.timestamp)
|
|
260
|
+
lines.append(f"[{ts}] {event.event_type.value}: {event.content}")
|
|
261
|
+
|
|
262
|
+
return "\n".join(lines)
|
|
263
|
+
|
|
264
|
+
except Exception as exc:
|
|
265
|
+
logger.exception("timeline tool error")
|
|
266
|
+
return f"Error: {exc}"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# Entry point
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def run_mcp_server() -> None:
|
|
275
|
+
"""Run the MCP server on stdio."""
|
|
276
|
+
logging.basicConfig(level=logging.WARNING)
|
|
277
|
+
mcp.run(transport="stdio")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Helpers
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _fmt_ts(ts_str: str) -> str:
|
|
286
|
+
try:
|
|
287
|
+
dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
288
|
+
return dt.astimezone().strftime("%Y-%m-%d %H:%M")
|
|
289
|
+
except ValueError:
|
|
290
|
+
return ts_str[:16]
|
recall/models.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""DevMem data models — Event dataclass and enums."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EventType(str, Enum):
|
|
12
|
+
TERMINAL_CMD = "terminal_cmd"
|
|
13
|
+
GIT_COMMIT = "git_commit"
|
|
14
|
+
GIT_BRANCH = "git_branch_switch"
|
|
15
|
+
GIT_PUSH = "git_push"
|
|
16
|
+
GIT_MERGE = "git_merge"
|
|
17
|
+
FILE_SAVE = "file_save"
|
|
18
|
+
FILE_CREATE = "file_create"
|
|
19
|
+
FILE_DELETE = "file_delete"
|
|
20
|
+
FILE_RENAME = "file_rename"
|
|
21
|
+
REPO_OPEN = "repo_open"
|
|
22
|
+
REPO_CLOSE = "repo_close"
|
|
23
|
+
AI_CHAT = "ai_chat"
|
|
24
|
+
DEBUG_SESSION = "debug_session"
|
|
25
|
+
TEST_RUN = "test_run"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Source(str, Enum):
|
|
29
|
+
SHELL_HOOK = "shell_hook"
|
|
30
|
+
GIT_HOOK = "git_hook"
|
|
31
|
+
GIT_POLLER = "git_poller"
|
|
32
|
+
VSCODE_EXT = "vscode_ext"
|
|
33
|
+
AI_CHAT_PARSER = "ai_chat_parser"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Event:
|
|
38
|
+
"""A single captured developer activity event."""
|
|
39
|
+
|
|
40
|
+
timestamp: str # ISO 8601 UTC, e.g. "2026-05-21T14:30:00Z"
|
|
41
|
+
date: str # YYYY-MM-DD derived from timestamp
|
|
42
|
+
event_type: EventType
|
|
43
|
+
source: Source
|
|
44
|
+
content: str # Human-readable text used for embedding & display
|
|
45
|
+
raw_data: dict[str, Any] # Original parsed data
|
|
46
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
47
|
+
repo_path: Optional[str] = None # Canonical absolute path
|
|
48
|
+
repo_name: Optional[str] = None # Basename
|
|
49
|
+
session_id: Optional[str] = None # UUID
|
|
50
|
+
embedding_id: Optional[int] = None
|
|
51
|
+
id: Optional[int] = None # Set after DB insert
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
# Serialization helpers
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def to_db_row(self) -> dict[str, Any]:
|
|
58
|
+
"""Return a dict suitable for inserting into the SQLite events table."""
|
|
59
|
+
return {
|
|
60
|
+
"timestamp": self.timestamp,
|
|
61
|
+
"date": self.date,
|
|
62
|
+
"event_type": self.event_type.value,
|
|
63
|
+
"source": self.source.value,
|
|
64
|
+
"content": self.content,
|
|
65
|
+
"raw_data": json.dumps(self.raw_data),
|
|
66
|
+
"metadata": json.dumps(self.metadata),
|
|
67
|
+
"repo_path": self.repo_path,
|
|
68
|
+
"repo_name": self.repo_name,
|
|
69
|
+
"session_id": self.session_id,
|
|
70
|
+
"embedding_id": self.embedding_id,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_db_row(cls, row: dict[str, Any]) -> "Event":
|
|
75
|
+
"""Reconstruct an Event from a SQLite row dict."""
|
|
76
|
+
return cls(
|
|
77
|
+
id=row["id"],
|
|
78
|
+
timestamp=row["timestamp"],
|
|
79
|
+
date=row["date"],
|
|
80
|
+
event_type=EventType(row["event_type"]),
|
|
81
|
+
source=Source(row["source"]),
|
|
82
|
+
content=row["content"],
|
|
83
|
+
raw_data=json.loads(row["raw_data"]) if row["raw_data"] else {},
|
|
84
|
+
metadata=json.loads(row["metadata"]) if row["metadata"] else {},
|
|
85
|
+
repo_path=row.get("repo_path"),
|
|
86
|
+
repo_name=row.get("repo_name"),
|
|
87
|
+
session_id=row.get("session_id"),
|
|
88
|
+
embedding_id=row.get("embedding_id"),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> dict[str, Any]:
|
|
92
|
+
"""Return a JSON-serializable dict."""
|
|
93
|
+
return {
|
|
94
|
+
"id": self.id,
|
|
95
|
+
"timestamp": self.timestamp,
|
|
96
|
+
"date": self.date,
|
|
97
|
+
"event_type": self.event_type.value,
|
|
98
|
+
"source": self.source.value,
|
|
99
|
+
"content": self.content,
|
|
100
|
+
"raw_data": self.raw_data,
|
|
101
|
+
"metadata": self.metadata,
|
|
102
|
+
"repo_path": self.repo_path,
|
|
103
|
+
"repo_name": self.repo_name,
|
|
104
|
+
"session_id": self.session_id,
|
|
105
|
+
"embedding_id": self.embedding_id,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Content text builders
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_content(event_type: EventType, data: dict[str, Any]) -> str:
|
|
115
|
+
"""Build the human-readable content string for an event from its raw data.
|
|
116
|
+
|
|
117
|
+
This is the text used for both display and embedding.
|
|
118
|
+
"""
|
|
119
|
+
match event_type:
|
|
120
|
+
case EventType.TERMINAL_CMD:
|
|
121
|
+
cmd = data.get("cmd", "")
|
|
122
|
+
repo = data.get("repo_name") or _basename(data.get("cwd", ""))
|
|
123
|
+
exit_code = data.get("exit_code", 0)
|
|
124
|
+
duration = data.get("duration_ms", 0)
|
|
125
|
+
return f"[terminal] {cmd} in {repo} (exit {exit_code}, {duration}ms)"
|
|
126
|
+
|
|
127
|
+
case EventType.GIT_COMMIT:
|
|
128
|
+
message = data.get("message", "")
|
|
129
|
+
branch = data.get("branch", "")
|
|
130
|
+
repo = data.get("repo_name", "")
|
|
131
|
+
files = data.get("files", [])
|
|
132
|
+
files_str = ", ".join(files[:5]) if files else ""
|
|
133
|
+
if len(files) > 5:
|
|
134
|
+
files_str += f" (+{len(files) - 5} more)"
|
|
135
|
+
return f"[git] commit '{message}' to {branch} in {repo}. Changed: {files_str}"
|
|
136
|
+
|
|
137
|
+
case EventType.GIT_BRANCH:
|
|
138
|
+
new_branch = data.get("new_branch", "")
|
|
139
|
+
old_branch = data.get("old_branch", "")
|
|
140
|
+
repo = data.get("repo_name", "")
|
|
141
|
+
return f"[git] switched to branch '{new_branch}' in {repo} (from '{old_branch}')"
|
|
142
|
+
|
|
143
|
+
case EventType.GIT_PUSH:
|
|
144
|
+
repo = data.get("repo_name", "")
|
|
145
|
+
branch = data.get("branch", "")
|
|
146
|
+
remote = data.get("remote", "origin")
|
|
147
|
+
count = data.get("commit_count", 0)
|
|
148
|
+
return f"[git] pushed {count} commit(s) on {branch} to {remote} in {repo}"
|
|
149
|
+
|
|
150
|
+
case EventType.GIT_MERGE:
|
|
151
|
+
repo = data.get("repo_name", "")
|
|
152
|
+
branch = data.get("branch", "")
|
|
153
|
+
merged = data.get("merged_branch", "")
|
|
154
|
+
squash = data.get("is_squash", False)
|
|
155
|
+
merge_type = "squash-merged" if squash else "merged"
|
|
156
|
+
merged_str = f" '{merged}'" if merged else ""
|
|
157
|
+
return f"[git] {merge_type}{merged_str} into {branch} in {repo}"
|
|
158
|
+
|
|
159
|
+
case EventType.FILE_SAVE:
|
|
160
|
+
filename = data.get("filename", "")
|
|
161
|
+
language = data.get("language", "")
|
|
162
|
+
repo = data.get("repo_name", "")
|
|
163
|
+
return f"[edit] saved {filename} ({language}) in {repo}"
|
|
164
|
+
|
|
165
|
+
case EventType.FILE_CREATE:
|
|
166
|
+
filename = data.get("filename", "")
|
|
167
|
+
repo = data.get("repo_name", "")
|
|
168
|
+
return f"[edit] created {filename} in {repo}"
|
|
169
|
+
|
|
170
|
+
case EventType.FILE_DELETE:
|
|
171
|
+
filename = data.get("filename", "")
|
|
172
|
+
repo = data.get("repo_name", "")
|
|
173
|
+
return f"[edit] deleted {filename} in {repo}"
|
|
174
|
+
|
|
175
|
+
case EventType.FILE_RENAME:
|
|
176
|
+
old = data.get("old_filename", "")
|
|
177
|
+
new = data.get("new_filename", "")
|
|
178
|
+
repo = data.get("repo_name", "")
|
|
179
|
+
return f"[edit] renamed {old} -> {new} in {repo}"
|
|
180
|
+
|
|
181
|
+
case EventType.REPO_OPEN:
|
|
182
|
+
repo = data.get("repo_name", "")
|
|
183
|
+
path = data.get("repo_path", "")
|
|
184
|
+
return f"[workspace] opened {repo} ({path})"
|
|
185
|
+
|
|
186
|
+
case EventType.REPO_CLOSE:
|
|
187
|
+
repo = data.get("repo_name", "")
|
|
188
|
+
duration = data.get("duration_minutes", 0)
|
|
189
|
+
return f"[workspace] closed {repo} after {duration} minutes"
|
|
190
|
+
|
|
191
|
+
case EventType.AI_CHAT:
|
|
192
|
+
source = data.get("ai_source", "ai")
|
|
193
|
+
role = data.get("role", "user")
|
|
194
|
+
preview = data.get("message_preview", "")[:150]
|
|
195
|
+
repo = data.get("repo_name", "")
|
|
196
|
+
suffix = f" in {repo}" if repo else ""
|
|
197
|
+
return f"[{source} chat] {role}: {preview}{suffix}"
|
|
198
|
+
|
|
199
|
+
case EventType.DEBUG_SESSION:
|
|
200
|
+
name = data.get("name", "")
|
|
201
|
+
repo = data.get("repo_name", "")
|
|
202
|
+
action = data.get("action", "started")
|
|
203
|
+
debug_type = data.get("debug_type", "")
|
|
204
|
+
type_str = f" ({debug_type})" if debug_type else ""
|
|
205
|
+
return f"[debug] {action} session '{name}'{type_str} in {repo}"
|
|
206
|
+
|
|
207
|
+
case EventType.TEST_RUN:
|
|
208
|
+
name = data.get("name", "")
|
|
209
|
+
repo = data.get("repo_name", "")
|
|
210
|
+
action = data.get("action", "started")
|
|
211
|
+
exit_code = data.get("exit_code")
|
|
212
|
+
if action == "finished" and exit_code is not None:
|
|
213
|
+
status = "passed" if exit_code == 0 else "failed"
|
|
214
|
+
return f"[test] '{name}' {status} in {repo}"
|
|
215
|
+
return f"[test] {action} '{name}' in {repo}"
|
|
216
|
+
|
|
217
|
+
case _:
|
|
218
|
+
return str(data)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _basename(path: str) -> str:
|
|
222
|
+
"""Return the last component of a path string."""
|
|
223
|
+
import os
|
|
224
|
+
|
|
225
|
+
return os.path.basename(path.rstrip("/")) or path
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Processor package."""
|