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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Shell collector — reads shell.tsv written by the zsh/bash hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, Optional
|
|
12
|
+
|
|
13
|
+
from watchdog.events import FileModifiedEvent, FileSystemEventHandler
|
|
14
|
+
from watchdog.observers import Observer
|
|
15
|
+
|
|
16
|
+
from recall.models import Event, EventType, Source, build_content
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# TSV column indices
|
|
21
|
+
_COL_TS = 0
|
|
22
|
+
_COL_CWD = 1
|
|
23
|
+
_COL_CMD = 2
|
|
24
|
+
_COL_EXIT = 3
|
|
25
|
+
_COL_DUR = 4
|
|
26
|
+
|
|
27
|
+
# KV key to persist read offset across daemon restarts
|
|
28
|
+
_OFFSET_KEY = "shell_tsv_offset"
|
|
29
|
+
|
|
30
|
+
# Command category prefixes — first matching category wins
|
|
31
|
+
# Each entry is (prefix_lowercase, category)
|
|
32
|
+
_CMD_CATEGORY_RULES: list[tuple[str, str]] = [
|
|
33
|
+
# test
|
|
34
|
+
("pytest", "test"),
|
|
35
|
+
("python -m pytest", "test"),
|
|
36
|
+
("python3 -m pytest", "test"),
|
|
37
|
+
("jest ", "test"),
|
|
38
|
+
("vitest", "test"),
|
|
39
|
+
("mocha ", "test"),
|
|
40
|
+
("go test", "test"),
|
|
41
|
+
("cargo test", "test"),
|
|
42
|
+
("npm test", "test"),
|
|
43
|
+
("yarn test", "test"),
|
|
44
|
+
("dotnet test", "test"),
|
|
45
|
+
("phpunit", "test"),
|
|
46
|
+
("rspec ", "test"),
|
|
47
|
+
("flutter test", "test"),
|
|
48
|
+
# build
|
|
49
|
+
("make", "build"),
|
|
50
|
+
("cmake", "build"),
|
|
51
|
+
("cargo build", "build"),
|
|
52
|
+
("cargo b ", "build"),
|
|
53
|
+
("gradle ", "build"),
|
|
54
|
+
("mvn compile", "build"),
|
|
55
|
+
("mvn package", "build"),
|
|
56
|
+
("mvn install", "build"),
|
|
57
|
+
("ant ", "build"),
|
|
58
|
+
("bazel build", "build"),
|
|
59
|
+
("ninja", "build"),
|
|
60
|
+
("msbuild", "build"),
|
|
61
|
+
("dotnet build", "build"),
|
|
62
|
+
("npm run build", "build"),
|
|
63
|
+
("yarn build", "build"),
|
|
64
|
+
("tsc ", "build"),
|
|
65
|
+
("tsc", "build"),
|
|
66
|
+
("go build", "build"),
|
|
67
|
+
("javac ", "build"),
|
|
68
|
+
("gcc ", "build"),
|
|
69
|
+
("g++ ", "build"),
|
|
70
|
+
("clang ", "build"),
|
|
71
|
+
# install
|
|
72
|
+
("pip install", "install"),
|
|
73
|
+
("pip3 install", "install"),
|
|
74
|
+
("npm install", "install"),
|
|
75
|
+
("npm i ", "install"),
|
|
76
|
+
("yarn add", "install"),
|
|
77
|
+
("yarn install", "install"),
|
|
78
|
+
("brew install", "install"),
|
|
79
|
+
("apt-get install", "install"),
|
|
80
|
+
("apt install", "install"),
|
|
81
|
+
("cargo add", "install"),
|
|
82
|
+
("go get", "install"),
|
|
83
|
+
("gem install", "install"),
|
|
84
|
+
("composer install", "install"),
|
|
85
|
+
("composer require", "install"),
|
|
86
|
+
("poetry add", "install"),
|
|
87
|
+
("uv add", "install"),
|
|
88
|
+
("uv pip install", "install"),
|
|
89
|
+
# deploy
|
|
90
|
+
("docker", "deploy"),
|
|
91
|
+
("kubectl ", "deploy"),
|
|
92
|
+
("terraform ", "deploy"),
|
|
93
|
+
("helm ", "deploy"),
|
|
94
|
+
("ansible", "deploy"),
|
|
95
|
+
("fly deploy", "deploy"),
|
|
96
|
+
("vercel ", "deploy"),
|
|
97
|
+
("netlify deploy", "deploy"),
|
|
98
|
+
# version control
|
|
99
|
+
("git push", "vcs"),
|
|
100
|
+
("git pull", "vcs"),
|
|
101
|
+
("git fetch", "vcs"),
|
|
102
|
+
("git merge", "vcs"),
|
|
103
|
+
("git rebase", "vcs"),
|
|
104
|
+
("git clone", "vcs"),
|
|
105
|
+
("git stash", "vcs"),
|
|
106
|
+
("git cherry-pick", "vcs"),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ShellCollector:
|
|
111
|
+
"""
|
|
112
|
+
Watches shell.tsv for new lines and emits terminal_cmd Events.
|
|
113
|
+
|
|
114
|
+
The collector is started inside the daemon and calls *event_callback*
|
|
115
|
+
for each new event parsed from the TSV file.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
shell_tsv: Path,
|
|
121
|
+
cmd_ignore_patterns: list[str],
|
|
122
|
+
event_callback: Callable[[Event], None],
|
|
123
|
+
get_offset: Callable[[], Optional[str]],
|
|
124
|
+
set_offset: Callable[[str], None],
|
|
125
|
+
) -> None:
|
|
126
|
+
self._path = shell_tsv
|
|
127
|
+
self._ignore_patterns = [p.lower() for p in cmd_ignore_patterns]
|
|
128
|
+
self._callback = event_callback
|
|
129
|
+
self._get_offset = get_offset
|
|
130
|
+
self._set_offset = set_offset
|
|
131
|
+
|
|
132
|
+
# Restore byte offset from KV store so we don't re-process old lines
|
|
133
|
+
stored = get_offset()
|
|
134
|
+
self._offset: int = int(stored) if stored else 0
|
|
135
|
+
|
|
136
|
+
self._observer: Optional[Observer] = None
|
|
137
|
+
self._lock = threading.Lock()
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Public
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def start(self) -> None:
|
|
144
|
+
"""Start the watchdog observer."""
|
|
145
|
+
# Ensure the file exists
|
|
146
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
self._path.touch(exist_ok=True)
|
|
148
|
+
|
|
149
|
+
# If the file grew while the daemon was stopped, catch up now
|
|
150
|
+
self._read_new_lines()
|
|
151
|
+
|
|
152
|
+
handler = _ShellTSVHandler(self._path, self._on_file_change)
|
|
153
|
+
self._observer = Observer()
|
|
154
|
+
self._observer.schedule(handler, str(self._path.parent), recursive=False)
|
|
155
|
+
self._observer.start()
|
|
156
|
+
logger.info("ShellCollector watching %s", self._path)
|
|
157
|
+
|
|
158
|
+
def stop(self) -> None:
|
|
159
|
+
if self._observer:
|
|
160
|
+
self._observer.stop()
|
|
161
|
+
self._observer.join(timeout=5)
|
|
162
|
+
logger.info("ShellCollector stopped")
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
# Internal
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def _on_file_change(self) -> None:
|
|
169
|
+
with self._lock:
|
|
170
|
+
self._read_new_lines()
|
|
171
|
+
|
|
172
|
+
def _read_new_lines(self) -> None:
|
|
173
|
+
"""Read any new bytes from shell.tsv since last offset."""
|
|
174
|
+
try:
|
|
175
|
+
file_size = self._path.stat().st_size
|
|
176
|
+
except OSError:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
if file_size <= self._offset:
|
|
180
|
+
# File was truncated/rotated — reset
|
|
181
|
+
if file_size < self._offset:
|
|
182
|
+
logger.warning("shell.tsv shrank; resetting offset")
|
|
183
|
+
self._offset = 0
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
with self._path.open("rb") as fh:
|
|
188
|
+
fh.seek(self._offset)
|
|
189
|
+
new_bytes = fh.read()
|
|
190
|
+
new_offset = fh.tell()
|
|
191
|
+
except OSError as exc:
|
|
192
|
+
logger.error("Cannot read shell.tsv: %s", exc)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
events: list[Event] = []
|
|
196
|
+
for raw_line in new_bytes.decode("utf-8", errors="replace").splitlines():
|
|
197
|
+
event = self._parse_line(raw_line.rstrip("\r\n"))
|
|
198
|
+
if event:
|
|
199
|
+
events.append(event)
|
|
200
|
+
|
|
201
|
+
self._offset = new_offset
|
|
202
|
+
self._set_offset(str(new_offset))
|
|
203
|
+
|
|
204
|
+
for event in events:
|
|
205
|
+
try:
|
|
206
|
+
self._callback(event)
|
|
207
|
+
except Exception:
|
|
208
|
+
logger.exception("Error in shell event callback")
|
|
209
|
+
|
|
210
|
+
def _parse_line(self, line: str) -> Optional[Event]:
|
|
211
|
+
"""Parse a single TSV line into an Event, or None if invalid/filtered."""
|
|
212
|
+
if not line.strip():
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
parts = line.split("\t")
|
|
216
|
+
if len(parts) < 5:
|
|
217
|
+
logger.debug("Skipping malformed shell.tsv line: %r", line[:100])
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
ts_str = parts[_COL_TS]
|
|
221
|
+
cwd = parts[_COL_CWD]
|
|
222
|
+
cmd = parts[_COL_CMD]
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
exit_code = int(parts[_COL_EXIT])
|
|
226
|
+
except ValueError:
|
|
227
|
+
exit_code = 0
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
duration_ms = int(parts[_COL_DUR])
|
|
231
|
+
except ValueError:
|
|
232
|
+
duration_ms = 0
|
|
233
|
+
|
|
234
|
+
# Privacy: skip commands matching ignore patterns
|
|
235
|
+
if self._is_sensitive(cmd):
|
|
236
|
+
logger.debug("Skipping sensitive command (pattern match)")
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
# Parse timestamp → date
|
|
240
|
+
try:
|
|
241
|
+
dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
242
|
+
date_str = dt.strftime("%Y-%m-%d")
|
|
243
|
+
except ValueError:
|
|
244
|
+
# Fallback to local now
|
|
245
|
+
dt = datetime.now(timezone.utc)
|
|
246
|
+
date_str = dt.strftime("%Y-%m-%d")
|
|
247
|
+
ts_str = dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
248
|
+
|
|
249
|
+
raw = {
|
|
250
|
+
"cmd": cmd,
|
|
251
|
+
"cwd": cwd,
|
|
252
|
+
"exit_code": exit_code,
|
|
253
|
+
"duration_ms": duration_ms,
|
|
254
|
+
"repo_name": os.path.basename(cwd.rstrip("/")) if cwd else "",
|
|
255
|
+
}
|
|
256
|
+
content = build_content(EventType.TERMINAL_CMD, raw)
|
|
257
|
+
|
|
258
|
+
metadata = {"cmd_category": _categorize_cmd(cmd)}
|
|
259
|
+
|
|
260
|
+
return Event(
|
|
261
|
+
timestamp=ts_str,
|
|
262
|
+
date=date_str,
|
|
263
|
+
event_type=EventType.TERMINAL_CMD,
|
|
264
|
+
source=Source.SHELL_HOOK,
|
|
265
|
+
content=content,
|
|
266
|
+
raw_data=raw,
|
|
267
|
+
metadata=metadata,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def _is_sensitive(self, cmd: str) -> bool:
|
|
271
|
+
"""Return True if the command matches any privacy ignore pattern."""
|
|
272
|
+
cmd_lower = cmd.lower()
|
|
273
|
+
for pattern in self._ignore_patterns:
|
|
274
|
+
if fnmatch.fnmatch(cmd_lower, pattern):
|
|
275
|
+
return True
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# Watchdog handler
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class _ShellTSVHandler(FileSystemEventHandler):
|
|
285
|
+
def __init__(self, path: Path, callback: Callable[[], None]) -> None:
|
|
286
|
+
self._path = str(path)
|
|
287
|
+
self._callback = callback
|
|
288
|
+
|
|
289
|
+
def on_modified(self, event: FileModifiedEvent) -> None:
|
|
290
|
+
if not event.is_directory and event.src_path == self._path:
|
|
291
|
+
self._callback()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _categorize_cmd(cmd: str) -> str:
|
|
295
|
+
"""Return a broad category for a shell command string."""
|
|
296
|
+
cmd_lower = cmd.lower().strip()
|
|
297
|
+
for prefix, category in _CMD_CATEGORY_RULES:
|
|
298
|
+
if cmd_lower.startswith(prefix):
|
|
299
|
+
return category
|
|
300
|
+
return "other"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""VS Code extension HTTP receiver — /event endpoint for the TypeScript extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Callable, Optional
|
|
9
|
+
|
|
10
|
+
from recall.models import Event, EventType, Source, build_content
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Map VS Code extension event types to our internal EventType
|
|
15
|
+
_TYPE_MAP: dict[str, EventType] = {
|
|
16
|
+
"workspace_open": EventType.REPO_OPEN,
|
|
17
|
+
"workspace_close": EventType.REPO_CLOSE,
|
|
18
|
+
"file_save": EventType.FILE_SAVE,
|
|
19
|
+
"active_time": EventType.REPO_OPEN, # treated as "still active" keep-alive
|
|
20
|
+
"file_create": EventType.FILE_CREATE,
|
|
21
|
+
"file_delete": EventType.FILE_DELETE,
|
|
22
|
+
"file_rename": EventType.FILE_RENAME,
|
|
23
|
+
"debug_session_start": EventType.DEBUG_SESSION,
|
|
24
|
+
"debug_session_end": EventType.DEBUG_SESSION,
|
|
25
|
+
"test_run_start": EventType.TEST_RUN,
|
|
26
|
+
"test_run_finish": EventType.TEST_RUN,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_vscode_event(
|
|
31
|
+
payload: dict,
|
|
32
|
+
event_callback: Callable[[Event], None],
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Parse an incoming JSON payload from the VS Code extension and emit an Event.
|
|
36
|
+
|
|
37
|
+
Payload fields (all optional except `type`):
|
|
38
|
+
type : str — workspace_open | workspace_close | file_save | active_time
|
|
39
|
+
ts : str — ISO 8601 timestamp (default: now)
|
|
40
|
+
workspace : str — workspace folder path
|
|
41
|
+
file : str — file path (for file_save)
|
|
42
|
+
language : str — language id (for file_save)
|
|
43
|
+
seconds_active : int — (for active_time)
|
|
44
|
+
"""
|
|
45
|
+
event_type_str = str(payload.get("type", ""))
|
|
46
|
+
event_type = _TYPE_MAP.get(event_type_str)
|
|
47
|
+
if event_type is None:
|
|
48
|
+
logger.debug("Ignoring unknown VS Code event type: %r", event_type_str)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
ts_str = str(payload.get("ts", ""))
|
|
52
|
+
try:
|
|
53
|
+
dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
54
|
+
ts_out = dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
55
|
+
date_str = dt.astimezone(timezone.utc).strftime("%Y-%m-%d")
|
|
56
|
+
except ValueError:
|
|
57
|
+
dt = datetime.now(timezone.utc)
|
|
58
|
+
ts_out = dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
59
|
+
date_str = dt.strftime("%Y-%m-%d")
|
|
60
|
+
|
|
61
|
+
workspace = str(payload.get("workspace", ""))
|
|
62
|
+
repo_path: Optional[str] = workspace if workspace else None
|
|
63
|
+
repo_name: Optional[str] = os.path.basename(workspace.rstrip("/")) if workspace else None
|
|
64
|
+
|
|
65
|
+
if event_type == EventType.FILE_SAVE:
|
|
66
|
+
file_path = str(payload.get("file", ""))
|
|
67
|
+
language = str(payload.get("language", ""))
|
|
68
|
+
filename = os.path.basename(file_path)
|
|
69
|
+
raw = {
|
|
70
|
+
"filename": filename,
|
|
71
|
+
"file_path": file_path,
|
|
72
|
+
"language": language,
|
|
73
|
+
"repo_name": repo_name or "",
|
|
74
|
+
"repo_path": workspace,
|
|
75
|
+
}
|
|
76
|
+
content = build_content(EventType.FILE_SAVE, raw)
|
|
77
|
+
|
|
78
|
+
elif event_type == EventType.FILE_CREATE:
|
|
79
|
+
file_path = str(payload.get("file", ""))
|
|
80
|
+
filename = str(payload.get("filename", "")) or os.path.basename(file_path)
|
|
81
|
+
raw = {
|
|
82
|
+
"filename": filename,
|
|
83
|
+
"file_path": file_path,
|
|
84
|
+
"repo_name": repo_name or "",
|
|
85
|
+
"repo_path": workspace,
|
|
86
|
+
}
|
|
87
|
+
content = build_content(EventType.FILE_CREATE, raw)
|
|
88
|
+
|
|
89
|
+
elif event_type == EventType.FILE_DELETE:
|
|
90
|
+
file_path = str(payload.get("file", ""))
|
|
91
|
+
filename = str(payload.get("filename", "")) or os.path.basename(file_path)
|
|
92
|
+
raw = {
|
|
93
|
+
"filename": filename,
|
|
94
|
+
"file_path": file_path,
|
|
95
|
+
"repo_name": repo_name or "",
|
|
96
|
+
"repo_path": workspace,
|
|
97
|
+
}
|
|
98
|
+
content = build_content(EventType.FILE_DELETE, raw)
|
|
99
|
+
|
|
100
|
+
elif event_type == EventType.FILE_RENAME:
|
|
101
|
+
old_file = str(payload.get("old_file", ""))
|
|
102
|
+
new_file = str(payload.get("new_file", ""))
|
|
103
|
+
old_filename = str(payload.get("old_filename", "")) or os.path.basename(old_file)
|
|
104
|
+
new_filename = str(payload.get("new_filename", "")) or os.path.basename(new_file)
|
|
105
|
+
raw = {
|
|
106
|
+
"old_filename": old_filename,
|
|
107
|
+
"new_filename": new_filename,
|
|
108
|
+
"old_file": old_file,
|
|
109
|
+
"new_file": new_file,
|
|
110
|
+
"repo_name": repo_name or "",
|
|
111
|
+
"repo_path": workspace,
|
|
112
|
+
}
|
|
113
|
+
content = build_content(EventType.FILE_RENAME, raw)
|
|
114
|
+
|
|
115
|
+
elif event_type == EventType.DEBUG_SESSION:
|
|
116
|
+
name = str(payload.get("name", ""))
|
|
117
|
+
debug_type = str(payload.get("debug_type", ""))
|
|
118
|
+
action = "started" if event_type_str == "debug_session_start" else "ended"
|
|
119
|
+
raw = {
|
|
120
|
+
"name": name,
|
|
121
|
+
"debug_type": debug_type,
|
|
122
|
+
"action": action,
|
|
123
|
+
"repo_name": repo_name or "",
|
|
124
|
+
"repo_path": workspace,
|
|
125
|
+
}
|
|
126
|
+
content = build_content(EventType.DEBUG_SESSION, raw)
|
|
127
|
+
|
|
128
|
+
elif event_type == EventType.TEST_RUN:
|
|
129
|
+
name = str(payload.get("name", ""))
|
|
130
|
+
action = "started" if event_type_str == "test_run_start" else "finished"
|
|
131
|
+
raw: dict = {
|
|
132
|
+
"name": name,
|
|
133
|
+
"action": action,
|
|
134
|
+
"repo_name": repo_name or "",
|
|
135
|
+
"repo_path": workspace,
|
|
136
|
+
}
|
|
137
|
+
if event_type_str == "test_run_finish":
|
|
138
|
+
raw["exit_code"] = int(payload.get("exit_code", 0))
|
|
139
|
+
content = build_content(EventType.TEST_RUN, raw)
|
|
140
|
+
|
|
141
|
+
elif event_type == EventType.REPO_OPEN:
|
|
142
|
+
raw = {
|
|
143
|
+
"repo_name": repo_name or workspace,
|
|
144
|
+
"repo_path": workspace,
|
|
145
|
+
"event": event_type_str,
|
|
146
|
+
}
|
|
147
|
+
content = build_content(EventType.REPO_OPEN, raw)
|
|
148
|
+
|
|
149
|
+
elif event_type == EventType.REPO_CLOSE:
|
|
150
|
+
raw = {
|
|
151
|
+
"repo_name": repo_name or workspace,
|
|
152
|
+
"repo_path": workspace,
|
|
153
|
+
"duration_minutes": 0,
|
|
154
|
+
}
|
|
155
|
+
content = build_content(EventType.REPO_CLOSE, raw)
|
|
156
|
+
|
|
157
|
+
else:
|
|
158
|
+
raw = dict(payload)
|
|
159
|
+
content = f"[vscode] {event_type_str}"
|
|
160
|
+
|
|
161
|
+
event = Event(
|
|
162
|
+
timestamp=ts_out,
|
|
163
|
+
date=date_str,
|
|
164
|
+
event_type=event_type,
|
|
165
|
+
source=Source.VSCODE_EXT,
|
|
166
|
+
content=content,
|
|
167
|
+
raw_data=raw,
|
|
168
|
+
repo_path=repo_path,
|
|
169
|
+
repo_name=repo_name,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
event_callback(event)
|
|
174
|
+
except Exception:
|
|
175
|
+
logger.exception("Error in VS Code event callback")
|