promty-collector 0.1.0
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.
- package/bin/promty.js +17 -0
- package/package.json +18 -0
- package/src/adapters/__init__.py +32 -0
- package/src/adapters/claude/__init__.py +1 -0
- package/src/adapters/claude/hook.py +11 -0
- package/src/adapters/codex/__init__.py +1 -0
- package/src/adapters/codex/hook.py +11 -0
- package/src/adapters/common.py +193 -0
- package/src/adapters/cursor/__init__.py +1 -0
- package/src/adapters/cursor/hook.py +11 -0
- package/src/adapters/gemini/__init__.py +1 -0
- package/src/adapters/gemini/hook.py +11 -0
- package/src/change_tracking.py +696 -0
- package/src/cli.py +1110 -0
- package/src/config.py +90 -0
- package/src/events.py +313 -0
- package/src/file_lock.py +38 -0
- package/src/git_context.py +69 -0
- package/src/payloads.py +58 -0
- package/src/response_capture.py +199 -0
- package/src/runtime_install.py +162 -0
- package/src/sequence.py +68 -0
- package/src/session_index.py +75 -0
- package/src/uploader/__init__.py +1 -0
- package/src/uploader/client.py +35 -0
- package/src/uploader/queue.py +130 -0
package/bin/promty.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const cliPath = path.join(packageRoot, "src", "cli.py");
|
|
9
|
+
const python = process.env.PROMTY_PYTHON || (process.platform === "win32" ? "python" : "python3");
|
|
10
|
+
const result = spawnSync(python, [cliPath, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
11
|
+
|
|
12
|
+
if (result.error) {
|
|
13
|
+
console.error(`Unable to start Promty with ${python}: ${result.error.message}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
process.exit(result.status ?? 1);
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "promty-collector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Promty local event collector",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"promty": "bin/promty.js",
|
|
9
|
+
"promty-collector": "bin/promty.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src/**/*.py"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from events import BaseEvent, EventType, normalize_tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def normalize_collector_event(
|
|
10
|
+
tool: str,
|
|
11
|
+
payload: Mapping[str, Any],
|
|
12
|
+
event_type: EventType | None = None,
|
|
13
|
+
) -> BaseEvent:
|
|
14
|
+
normalized_tool = normalize_tool(tool)
|
|
15
|
+
|
|
16
|
+
if normalized_tool == "claude-code":
|
|
17
|
+
from adapters.claude.hook import normalize
|
|
18
|
+
|
|
19
|
+
return normalize(payload, event_type)
|
|
20
|
+
if normalized_tool == "codex-cli":
|
|
21
|
+
from adapters.codex.hook import normalize
|
|
22
|
+
|
|
23
|
+
return normalize(payload, event_type)
|
|
24
|
+
if normalized_tool == "cursor":
|
|
25
|
+
from adapters.cursor.hook import normalize
|
|
26
|
+
|
|
27
|
+
return normalize(payload, event_type)
|
|
28
|
+
if normalized_tool == "gemini-cli":
|
|
29
|
+
from adapters.gemini.hook import normalize
|
|
30
|
+
|
|
31
|
+
return normalize(payload, event_type)
|
|
32
|
+
raise ValueError(f"Unsupported tool: {tool}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from adapters.common import normalize_tool_event
|
|
7
|
+
from events import BaseEvent, EventType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize(payload: Mapping[str, Any], event_type: EventType | None = None) -> BaseEvent:
|
|
11
|
+
return normalize_tool_event("claude-code", payload, event_type)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from adapters.common import normalize_tool_event
|
|
7
|
+
from events import BaseEvent, EventType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize(payload: Mapping[str, Any], event_type: EventType | None = None) -> BaseEvent:
|
|
11
|
+
return normalize_tool_event("codex-cli", payload, event_type)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from events import (
|
|
7
|
+
SUPPORTED_EVENT_TYPES,
|
|
8
|
+
BaseEvent,
|
|
9
|
+
CommitCreatedPayload,
|
|
10
|
+
EventType,
|
|
11
|
+
FilesChangedPayload,
|
|
12
|
+
PromptSubmittedPayload,
|
|
13
|
+
ResponseReceivedPayload,
|
|
14
|
+
SessionEndedPayload,
|
|
15
|
+
SessionStartedPayload,
|
|
16
|
+
SupportedTool,
|
|
17
|
+
build_event,
|
|
18
|
+
normalize_event_type,
|
|
19
|
+
)
|
|
20
|
+
from git_context import git_context
|
|
21
|
+
from payloads import (
|
|
22
|
+
SESSION_ID_KEYS,
|
|
23
|
+
TURN_ID_KEYS,
|
|
24
|
+
WORKSPACE_KEYS,
|
|
25
|
+
first_bool,
|
|
26
|
+
first_int,
|
|
27
|
+
first_string,
|
|
28
|
+
first_value,
|
|
29
|
+
payload_views,
|
|
30
|
+
string_list,
|
|
31
|
+
)
|
|
32
|
+
from response_capture import response_payload_fields
|
|
33
|
+
|
|
34
|
+
EXTERNAL_EVENT_ALIASES: dict[str, EventType] = {
|
|
35
|
+
"SessionStart": "SessionStarted",
|
|
36
|
+
"SessionEnd": "SessionEnded",
|
|
37
|
+
"Stop": "ResponseReceived",
|
|
38
|
+
"UserPromptSubmit": "PromptSubmitted",
|
|
39
|
+
"user_prompt_submit": "PromptSubmitted",
|
|
40
|
+
"prompt_sent": "PromptSubmitted",
|
|
41
|
+
"prompt_submitted": "PromptSubmitted",
|
|
42
|
+
"session_started": "SessionStarted",
|
|
43
|
+
"session_ended": "SessionEnded",
|
|
44
|
+
"files_changed": "FilesChanged",
|
|
45
|
+
"commit_created": "CommitCreated",
|
|
46
|
+
"response_received": "ResponseReceived",
|
|
47
|
+
"prompt_response": "ResponseReceived",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def infer_event_type(raw_payload: Mapping[str, Any], default: EventType = "PromptSubmitted") -> EventType:
|
|
52
|
+
explicit_event_type = raw_payload.get("event_type")
|
|
53
|
+
if isinstance(explicit_event_type, str):
|
|
54
|
+
return normalize_event_type(explicit_event_type)
|
|
55
|
+
|
|
56
|
+
for key in ("hook_event_name", "type", "event"):
|
|
57
|
+
raw_event_type = raw_payload.get(key)
|
|
58
|
+
if not isinstance(raw_event_type, str):
|
|
59
|
+
continue
|
|
60
|
+
if raw_event_type in SUPPORTED_EVENT_TYPES:
|
|
61
|
+
return normalize_event_type(raw_event_type)
|
|
62
|
+
alias = EXTERNAL_EVENT_ALIASES.get(raw_event_type)
|
|
63
|
+
if alias:
|
|
64
|
+
return alias
|
|
65
|
+
|
|
66
|
+
return default
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def normalize_event_payload(
|
|
70
|
+
event_type: EventType,
|
|
71
|
+
raw_payload: Mapping[str, Any],
|
|
72
|
+
) -> (
|
|
73
|
+
SessionStartedPayload
|
|
74
|
+
| PromptSubmittedPayload
|
|
75
|
+
| ResponseReceivedPayload
|
|
76
|
+
| FilesChangedPayload
|
|
77
|
+
| CommitCreatedPayload
|
|
78
|
+
| SessionEndedPayload
|
|
79
|
+
):
|
|
80
|
+
payloads = payload_views(raw_payload)
|
|
81
|
+
cwd = first_string(payloads, WORKSPACE_KEYS)
|
|
82
|
+
|
|
83
|
+
class LazyGitContext(dict[str, str]):
|
|
84
|
+
def __init__(self) -> None:
|
|
85
|
+
super().__init__()
|
|
86
|
+
self._loaded = False
|
|
87
|
+
|
|
88
|
+
def get(self, key: str, default: str | None = None) -> str | None:
|
|
89
|
+
if not self._loaded:
|
|
90
|
+
self.update(git_context(cwd))
|
|
91
|
+
self._loaded = True
|
|
92
|
+
return super().get(key, default)
|
|
93
|
+
|
|
94
|
+
git = LazyGitContext()
|
|
95
|
+
|
|
96
|
+
if event_type == "SessionStarted":
|
|
97
|
+
return SessionStartedPayload(
|
|
98
|
+
cwd=cwd,
|
|
99
|
+
branch=first_string(payloads, ("branch", "git_branch")) or git.get("branch"),
|
|
100
|
+
git_remote=first_string(payloads, ("git_remote", "remote_url")) or git.get("git_remote"),
|
|
101
|
+
github_url=first_string(payloads, ("github_url", "repository_url")) or git.get("github_url"),
|
|
102
|
+
model=first_string(payloads, ("model", "model_name")),
|
|
103
|
+
permission_mode=first_string(payloads, ("permission_mode",)),
|
|
104
|
+
session_id=first_string(payloads, SESSION_ID_KEYS),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if event_type == "PromptSubmitted":
|
|
108
|
+
prompt = first_string(payloads, ("prompt", "input", "text", "message"))
|
|
109
|
+
if not prompt or not prompt.strip():
|
|
110
|
+
raise ValueError("Hook payload is missing a non-empty prompt/input")
|
|
111
|
+
|
|
112
|
+
return PromptSubmittedPayload(
|
|
113
|
+
prompt=prompt,
|
|
114
|
+
cwd=cwd,
|
|
115
|
+
model=first_string(payloads, ("model", "model_name")),
|
|
116
|
+
permission_mode=first_string(payloads, ("permission_mode",)),
|
|
117
|
+
transcript_path=first_string(payloads, ("transcript_path", "transcript")),
|
|
118
|
+
turn_id=first_value(payloads, TURN_ID_KEYS),
|
|
119
|
+
session_id=first_string(payloads, SESSION_ID_KEYS),
|
|
120
|
+
branch=first_string(payloads, ("branch", "git_branch")) or git.get("branch"),
|
|
121
|
+
git_remote=first_string(payloads, ("git_remote", "remote_url")) or git.get("git_remote"),
|
|
122
|
+
github_url=first_string(payloads, ("github_url", "repository_url")) or git.get("github_url"),
|
|
123
|
+
hook_event_name=first_string(payloads, ("hook_event_name",)),
|
|
124
|
+
approval_policy=first_string(payloads, ("approval_policy",)),
|
|
125
|
+
sandbox_mode=first_string(payloads, ("sandbox_mode",)),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if event_type == "ResponseReceived":
|
|
129
|
+
return ResponseReceivedPayload(
|
|
130
|
+
**response_payload_fields(raw_payload),
|
|
131
|
+
duration_ms=first_int(payloads, ("duration_ms", "elapsed_ms")),
|
|
132
|
+
success=first_bool(payloads, ("success", "ok")),
|
|
133
|
+
model=first_string(payloads, ("model", "model_name")),
|
|
134
|
+
session_id=first_string(payloads, SESSION_ID_KEYS),
|
|
135
|
+
turn_id=first_value(payloads, TURN_ID_KEYS),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if event_type == "FilesChanged":
|
|
139
|
+
return FilesChangedPayload(
|
|
140
|
+
files=string_list(payloads, ("files", "changed_files", "paths")),
|
|
141
|
+
cwd=cwd,
|
|
142
|
+
session_id=first_string(payloads, SESSION_ID_KEYS),
|
|
143
|
+
prompt_event_id=first_string(payloads, ("prompt_event_id",)),
|
|
144
|
+
turn_id=first_value(payloads, TURN_ID_KEYS),
|
|
145
|
+
git_root=first_string(payloads, ("git_root",)) or git.get("git_root"),
|
|
146
|
+
branch=first_string(payloads, ("branch", "git_branch")) or git.get("branch"),
|
|
147
|
+
git_remote=first_string(payloads, ("git_remote", "remote_url")) or git.get("git_remote"),
|
|
148
|
+
github_url=first_string(payloads, ("github_url", "repository_url")) or git.get("github_url"),
|
|
149
|
+
base_commit=first_string(payloads, ("base_commit",)),
|
|
150
|
+
head_commit=first_string(payloads, ("head_commit",)),
|
|
151
|
+
baseline_captured_at=first_string(payloads, ("baseline_captured_at",)),
|
|
152
|
+
detected_at=first_string(payloads, ("detected_at",)),
|
|
153
|
+
source=first_string(payloads, ("source",)),
|
|
154
|
+
summary=first_value(payloads, ("summary",)),
|
|
155
|
+
changes=first_value(payloads, ("changes",)) or [],
|
|
156
|
+
change_detection_complete=first_bool(payloads, ("change_detection_complete",)),
|
|
157
|
+
no_changes=first_bool(payloads, ("no_changes",)),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if event_type == "CommitCreated":
|
|
161
|
+
return CommitCreatedPayload(
|
|
162
|
+
hash=first_string(payloads, ("hash", "commit_hash", "sha")),
|
|
163
|
+
message=first_string(payloads, ("message", "commit_message")),
|
|
164
|
+
branch=first_string(payloads, ("branch", "git_branch")) or git.get("branch"),
|
|
165
|
+
git_remote=first_string(payloads, ("git_remote", "remote_url")) or git.get("git_remote"),
|
|
166
|
+
github_url=first_string(payloads, ("github_url", "repository_url")) or git.get("github_url"),
|
|
167
|
+
cwd=cwd,
|
|
168
|
+
session_id=first_string(payloads, SESSION_ID_KEYS),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if event_type == "SessionEnded":
|
|
172
|
+
return SessionEndedPayload(
|
|
173
|
+
reason=first_string(payloads, ("reason", "exit_reason")),
|
|
174
|
+
duration=first_int(payloads, ("duration", "duration_seconds")),
|
|
175
|
+
session_id=first_string(payloads, SESSION_ID_KEYS),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
raise ValueError(f"Unsupported event type: {event_type}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def normalize_tool_event(
|
|
182
|
+
tool: SupportedTool,
|
|
183
|
+
raw_payload: Mapping[str, Any],
|
|
184
|
+
event_type: EventType | None = None,
|
|
185
|
+
) -> BaseEvent:
|
|
186
|
+
event_type = event_type or infer_event_type(raw_payload)
|
|
187
|
+
normalized_payload = normalize_event_payload(event_type, raw_payload)
|
|
188
|
+
return build_event(
|
|
189
|
+
tool=tool,
|
|
190
|
+
event_type=event_type,
|
|
191
|
+
payload=normalized_payload,
|
|
192
|
+
raw_payload=dict(raw_payload),
|
|
193
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from adapters.common import normalize_tool_event
|
|
7
|
+
from events import BaseEvent, EventType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize(payload: Mapping[str, Any], event_type: EventType | None = None) -> BaseEvent:
|
|
11
|
+
return normalize_tool_event("cursor", payload, event_type)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from adapters.common import normalize_tool_event
|
|
7
|
+
from events import BaseEvent, EventType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize(payload: Mapping[str, Any], event_type: EventType | None = None) -> BaseEvent:
|
|
11
|
+
return normalize_tool_event("gemini-cli", payload, event_type)
|