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 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)