promty-collector 0.1.0 → 0.1.2
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/package.json +5 -1
- package/src/cli.py +81 -1
- package/src/runtime_install.py +2 -0
- package/src/updater.py +57 -0
- package/src/uploader/client.py +6 -1
- package/src/version.py +1 -0
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promty-collector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Promty local event collector",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/NaHyeongyu/BuildHub.git"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"bin": {
|
|
8
12
|
"promty": "bin/promty.js",
|
package/src/cli.py
CHANGED
|
@@ -5,6 +5,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import secrets
|
|
8
|
+
import shlex
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
import time
|
|
@@ -40,13 +41,16 @@ from payloads import (
|
|
|
40
41
|
WORKSPACE_KEYS,
|
|
41
42
|
get_first_string,
|
|
42
43
|
)
|
|
43
|
-
from runtime_install import install_runtime, quote_command_path
|
|
44
|
+
from runtime_install import install_runtime, launcher_path, quote_command_path
|
|
44
45
|
from sequence import SequenceStore
|
|
45
46
|
from session_index import SessionIndex
|
|
46
47
|
from uploader.client import PromptHubUploader
|
|
47
48
|
from uploader.queue import JSONLQueue
|
|
49
|
+
from updater import auto_update
|
|
50
|
+
from version import COLLECTOR_VERSION
|
|
48
51
|
|
|
49
52
|
InstallTarget = Literal["all", "claude-code", "codex-cli"]
|
|
53
|
+
Profile = Literal["dev", "prod"]
|
|
50
54
|
INSTALL_TOOLS: tuple[SupportedTool, ...] = ("codex-cli", "claude-code")
|
|
51
55
|
INSTALL_TOOL_ALIASES: dict[str, InstallTarget] = {
|
|
52
56
|
"all": "all",
|
|
@@ -55,6 +59,11 @@ INSTALL_TOOL_ALIASES: dict[str, InstallTarget] = {
|
|
|
55
59
|
"codex": "codex-cli",
|
|
56
60
|
"codex-cli": "codex-cli",
|
|
57
61
|
}
|
|
62
|
+
PROFILE_URLS: dict[Profile, tuple[str, str]] = {
|
|
63
|
+
"dev": ("http://127.0.0.1:5173", "http://127.0.0.1:8011"),
|
|
64
|
+
"prod": ("https://promty.org", "https://api.promty.org"),
|
|
65
|
+
}
|
|
66
|
+
AUTO_UPDATE_INTERVAL_SECONDS = 6 * 60 * 60
|
|
58
67
|
CODEX_HOOKS: tuple[dict[str, Any], ...] = (
|
|
59
68
|
{
|
|
60
69
|
"event": "UserPromptSubmit",
|
|
@@ -184,6 +193,26 @@ def _tools_for_install_target(target: InstallTarget) -> tuple[SupportedTool, ...
|
|
|
184
193
|
return (target,)
|
|
185
194
|
|
|
186
195
|
|
|
196
|
+
def _apply_profile_defaults(args: argparse.Namespace) -> None:
|
|
197
|
+
profile = getattr(args, "profile", None)
|
|
198
|
+
if not profile:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
profile_root = Path("~/.prompthub/profiles").expanduser() / profile
|
|
202
|
+
app_url, api_url = PROFILE_URLS[profile]
|
|
203
|
+
defaults = {
|
|
204
|
+
"app_url": app_url,
|
|
205
|
+
"api_url": api_url,
|
|
206
|
+
"config_path": str(profile_root / "config.json"),
|
|
207
|
+
"queue_path": str(profile_root / "events"),
|
|
208
|
+
"pid_path": str(profile_root / "uploader.pid"),
|
|
209
|
+
"log_path": str(profile_root / "uploader.log"),
|
|
210
|
+
}
|
|
211
|
+
for name, value in defaults.items():
|
|
212
|
+
if hasattr(args, name) and getattr(args, name) is None:
|
|
213
|
+
setattr(args, name, value)
|
|
214
|
+
|
|
215
|
+
|
|
187
216
|
def _apply_known_session(
|
|
188
217
|
*,
|
|
189
218
|
index: SessionIndex,
|
|
@@ -348,8 +377,20 @@ def upload(args: argparse.Namespace) -> int:
|
|
|
348
377
|
f"{JSONLQueue(args.queue_path).path} -> {api_url} "
|
|
349
378
|
f"every {interval:g}s"
|
|
350
379
|
)
|
|
380
|
+
next_update_check = 0.0
|
|
351
381
|
while True:
|
|
352
382
|
try:
|
|
383
|
+
now = time.monotonic()
|
|
384
|
+
if not args.no_auto_update and now >= next_update_check:
|
|
385
|
+
next_update_check = now + AUTO_UPDATE_INTERVAL_SECONDS
|
|
386
|
+
updated_version = auto_update()
|
|
387
|
+
if updated_version:
|
|
388
|
+
print(
|
|
389
|
+
f"Promty updated {COLLECTOR_VERSION} -> {updated_version}; restarting",
|
|
390
|
+
flush=True,
|
|
391
|
+
)
|
|
392
|
+
launcher = launcher_path()
|
|
393
|
+
os.execv(str(launcher), [str(launcher), *sys.argv[1:]])
|
|
353
394
|
uploaded_count = _upload_queued_events(args)
|
|
354
395
|
if uploaded_count:
|
|
355
396
|
print(f"Uploaded {uploaded_count} events", flush=True)
|
|
@@ -615,6 +656,7 @@ def _install_codex_hooks(
|
|
|
615
656
|
command_prefix: str,
|
|
616
657
|
normalized_tool: SupportedTool,
|
|
617
658
|
repo_root: Path,
|
|
659
|
+
queue_path: str | None = None,
|
|
618
660
|
) -> int:
|
|
619
661
|
hooks_path = repo_root / ".codex" / "hooks.json"
|
|
620
662
|
config = _read_hooks_config(hooks_path)
|
|
@@ -622,6 +664,8 @@ def _install_codex_hooks(
|
|
|
622
664
|
|
|
623
665
|
for spec in CODEX_HOOKS:
|
|
624
666
|
command = f"{command_prefix} {spec['subcommand']} --tool {normalized_tool}"
|
|
667
|
+
if queue_path:
|
|
668
|
+
command += f" --queue-path {shlex.quote(str(Path(queue_path).expanduser()))}"
|
|
625
669
|
hook = {
|
|
626
670
|
"type": "command",
|
|
627
671
|
"command": command,
|
|
@@ -649,6 +693,7 @@ def _install_claude_hooks(
|
|
|
649
693
|
command_prefix: str,
|
|
650
694
|
normalized_tool: SupportedTool,
|
|
651
695
|
repo_root: Path,
|
|
696
|
+
queue_path: str | None = None,
|
|
652
697
|
) -> int:
|
|
653
698
|
settings_path = repo_root / ".claude" / "settings.local.json"
|
|
654
699
|
config = _read_hooks_config(settings_path)
|
|
@@ -656,6 +701,8 @@ def _install_claude_hooks(
|
|
|
656
701
|
|
|
657
702
|
for spec in CLAUDE_HOOKS:
|
|
658
703
|
command = f"{command_prefix} {spec['subcommand']} --tool {normalized_tool}"
|
|
704
|
+
if queue_path:
|
|
705
|
+
command += f" --queue-path {shlex.quote(str(Path(queue_path).expanduser()))}"
|
|
659
706
|
hook = {
|
|
660
707
|
"type": "command",
|
|
661
708
|
"command": command,
|
|
@@ -687,6 +734,8 @@ def install_hooks(args: argparse.Namespace) -> int:
|
|
|
687
734
|
command_prefix = quote_command_path(launcher_path)
|
|
688
735
|
print(f"Promty runtime ready: {launcher_path}")
|
|
689
736
|
|
|
737
|
+
queue_path = getattr(args, "queue_path", None)
|
|
738
|
+
|
|
690
739
|
failures: list[tuple[SupportedTool, Exception]] = []
|
|
691
740
|
for normalized_tool in _tools_for_install_target(target):
|
|
692
741
|
try:
|
|
@@ -695,12 +744,14 @@ def install_hooks(args: argparse.Namespace) -> int:
|
|
|
695
744
|
command_prefix=command_prefix,
|
|
696
745
|
normalized_tool=normalized_tool,
|
|
697
746
|
repo_root=repo_root,
|
|
747
|
+
queue_path=queue_path,
|
|
698
748
|
)
|
|
699
749
|
elif normalized_tool == "claude-code":
|
|
700
750
|
_install_claude_hooks(
|
|
701
751
|
command_prefix=command_prefix,
|
|
702
752
|
normalized_tool=normalized_tool,
|
|
703
753
|
repo_root=repo_root,
|
|
754
|
+
queue_path=queue_path,
|
|
704
755
|
)
|
|
705
756
|
else:
|
|
706
757
|
raise AssertionError(f"Unexpected hook tool: {normalized_tool}")
|
|
@@ -757,6 +808,10 @@ def start_uploader(args: argparse.Namespace) -> int:
|
|
|
757
808
|
"--interval",
|
|
758
809
|
str(max(args.interval, 0.25)),
|
|
759
810
|
]
|
|
811
|
+
if getattr(args, "queue_path", None):
|
|
812
|
+
command.extend(["--queue-path", str(Path(args.queue_path).expanduser())])
|
|
813
|
+
if getattr(args, "no_auto_update", False):
|
|
814
|
+
command.append("--no-auto-update")
|
|
760
815
|
process = subprocess.Popen(
|
|
761
816
|
command,
|
|
762
817
|
stdout=log_file,
|
|
@@ -910,6 +965,7 @@ def init(args: argparse.Namespace) -> int:
|
|
|
910
965
|
source=None,
|
|
911
966
|
repo_root=args.repo_root,
|
|
912
967
|
hook_command=args.hook_command,
|
|
968
|
+
queue_path=args.queue_path,
|
|
913
969
|
)
|
|
914
970
|
hooks_result = install_hooks(install_args)
|
|
915
971
|
|
|
@@ -920,6 +976,8 @@ def init(args: argparse.Namespace) -> int:
|
|
|
920
976
|
interval=args.upload_interval,
|
|
921
977
|
log_path=args.log_path,
|
|
922
978
|
pid_path=args.pid_path,
|
|
979
|
+
queue_path=args.queue_path,
|
|
980
|
+
no_auto_update=args.no_auto_update,
|
|
923
981
|
token=args.token,
|
|
924
982
|
)
|
|
925
983
|
start_uploader(uploader_args)
|
|
@@ -997,6 +1055,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
997
1055
|
upload_parser.add_argument("--token", default=None)
|
|
998
1056
|
upload_parser.add_argument("--config-path")
|
|
999
1057
|
upload_parser.add_argument("--queue-path")
|
|
1058
|
+
upload_parser.add_argument("--profile", choices=sorted(PROFILE_URLS))
|
|
1059
|
+
upload_parser.add_argument("--no-auto-update", action="store_true")
|
|
1000
1060
|
upload_parser.add_argument("--limit", type=int, default=100)
|
|
1001
1061
|
upload_parser.add_argument("--timeout", type=float, default=10)
|
|
1002
1062
|
upload_parser.add_argument(
|
|
@@ -1017,6 +1077,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1017
1077
|
login_parser.add_argument("--api-url")
|
|
1018
1078
|
login_parser.add_argument("--callback-port", type=int, default=0)
|
|
1019
1079
|
login_parser.add_argument("--config-path")
|
|
1080
|
+
login_parser.add_argument("--profile", choices=sorted(PROFILE_URLS))
|
|
1020
1081
|
login_parser.add_argument("--no-browser", action="store_true")
|
|
1021
1082
|
login_parser.add_argument("--timeout", type=float, default=180)
|
|
1022
1083
|
login_parser.add_argument("--token")
|
|
@@ -1036,6 +1097,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1036
1097
|
help="Deprecated alias for --tool",
|
|
1037
1098
|
)
|
|
1038
1099
|
install_hooks_parser.add_argument("--repo-root")
|
|
1100
|
+
install_hooks_parser.add_argument("--profile", choices=sorted(PROFILE_URLS))
|
|
1101
|
+
install_hooks_parser.add_argument("--queue-path")
|
|
1039
1102
|
install_hooks_parser.add_argument(
|
|
1040
1103
|
"--hook-command",
|
|
1041
1104
|
help="Command prefix that the AI tool should call before the hook subcommand.",
|
|
@@ -1048,6 +1111,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1048
1111
|
start_uploader_parser.add_argument("--interval", type=float, default=2)
|
|
1049
1112
|
start_uploader_parser.add_argument("--log-path")
|
|
1050
1113
|
start_uploader_parser.add_argument("--pid-path")
|
|
1114
|
+
start_uploader_parser.add_argument("--profile", choices=sorted(PROFILE_URLS))
|
|
1115
|
+
start_uploader_parser.add_argument("--queue-path")
|
|
1116
|
+
start_uploader_parser.add_argument("--no-auto-update", action="store_true")
|
|
1051
1117
|
start_uploader_parser.add_argument("--token")
|
|
1052
1118
|
start_uploader_parser.set_defaults(func=start_uploader)
|
|
1053
1119
|
|
|
@@ -1059,6 +1125,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1059
1125
|
doctor_parser.add_argument("--repo-root")
|
|
1060
1126
|
doctor_parser.add_argument("--timeout", type=float, default=3)
|
|
1061
1127
|
doctor_parser.add_argument("--token")
|
|
1128
|
+
doctor_parser.add_argument("--profile", choices=sorted(PROFILE_URLS))
|
|
1062
1129
|
doctor_parser.add_argument(
|
|
1063
1130
|
"--tool",
|
|
1064
1131
|
choices=sorted(INSTALL_TOOL_ALIASES),
|
|
@@ -1076,6 +1143,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1076
1143
|
init_parser.add_argument("--log-path")
|
|
1077
1144
|
init_parser.add_argument("--no-browser", action="store_true")
|
|
1078
1145
|
init_parser.add_argument("--pid-path")
|
|
1146
|
+
init_parser.add_argument("--profile", choices=sorted(PROFILE_URLS))
|
|
1147
|
+
init_parser.add_argument("--queue-path")
|
|
1148
|
+
init_parser.add_argument("--no-auto-update", action="store_true")
|
|
1079
1149
|
init_parser.add_argument("--repo-root")
|
|
1080
1150
|
init_parser.add_argument("--skip-login", action="store_true")
|
|
1081
1151
|
init_parser.add_argument("--skip-uploader", action="store_true")
|
|
@@ -1090,12 +1160,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1090
1160
|
init_parser.add_argument("--upload-interval", type=float, default=2)
|
|
1091
1161
|
init_parser.set_defaults(func=init)
|
|
1092
1162
|
|
|
1163
|
+
update_runtime_parser = subparsers.add_parser("update-runtime")
|
|
1164
|
+
update_runtime_parser.set_defaults(func=lambda _: _update_runtime())
|
|
1165
|
+
|
|
1093
1166
|
return parser
|
|
1094
1167
|
|
|
1095
1168
|
|
|
1169
|
+
def _update_runtime() -> int:
|
|
1170
|
+
path = install_runtime()
|
|
1171
|
+
print(f"Promty {COLLECTOR_VERSION} runtime ready: {path}")
|
|
1172
|
+
return 0
|
|
1173
|
+
|
|
1174
|
+
|
|
1096
1175
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
1097
1176
|
parser = build_parser()
|
|
1098
1177
|
args = parser.parse_args(argv)
|
|
1178
|
+
_apply_profile_defaults(args)
|
|
1099
1179
|
try:
|
|
1100
1180
|
return args.func(args)
|
|
1101
1181
|
except KeyboardInterrupt:
|
package/src/runtime_install.py
CHANGED
package/src/updater.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from urllib import request
|
|
7
|
+
|
|
8
|
+
from version import COLLECTOR_VERSION
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
NPM_PACKAGE = "promty-collector"
|
|
12
|
+
NPM_REGISTRY_URL = f"https://registry.npmjs.org/{NPM_PACKAGE}/latest"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _version_tuple(value: str) -> tuple[int, ...]:
|
|
16
|
+
core = value.strip().split("-", 1)[0]
|
|
17
|
+
try:
|
|
18
|
+
return tuple(int(part) for part in core.split("."))
|
|
19
|
+
except ValueError:
|
|
20
|
+
return ()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def latest_version(timeout: float = 5) -> str | None:
|
|
24
|
+
with request.urlopen(NPM_REGISTRY_URL, timeout=timeout) as response:
|
|
25
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
26
|
+
version = payload.get("version")
|
|
27
|
+
return version.strip() if isinstance(version, str) and version.strip() else None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def update_available(current: str, latest: str) -> bool:
|
|
31
|
+
current_parts = _version_tuple(current)
|
|
32
|
+
latest_parts = _version_tuple(latest)
|
|
33
|
+
return bool(current_parts and latest_parts and latest_parts > current_parts)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def install_latest(version: str) -> bool:
|
|
37
|
+
npx = shutil.which("npx")
|
|
38
|
+
if npx is None:
|
|
39
|
+
return False
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
[npx, "--yes", f"{NPM_PACKAGE}@{version}", "update-runtime"],
|
|
42
|
+
check=False,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=120,
|
|
46
|
+
)
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
message = result.stderr.strip() or result.stdout.strip() or "unknown error"
|
|
49
|
+
raise RuntimeError(f"automatic update failed: {message}")
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def auto_update() -> str | None:
|
|
54
|
+
newest = latest_version()
|
|
55
|
+
if newest is None or not update_available(COLLECTOR_VERSION, newest):
|
|
56
|
+
return None
|
|
57
|
+
return newest if install_latest(newest) else None
|
package/src/uploader/client.py
CHANGED
|
@@ -4,6 +4,8 @@ import json
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
from urllib import request
|
|
6
6
|
|
|
7
|
+
from version import COLLECTOR_VERSION
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
class PromptHubUploader:
|
|
9
11
|
def __init__(self, api_url: str, token: str | None = None, timeout: float = 10) -> None:
|
|
@@ -16,7 +18,10 @@ class PromptHubUploader:
|
|
|
16
18
|
return []
|
|
17
19
|
|
|
18
20
|
body = json.dumps({"events": events}).encode("utf-8")
|
|
19
|
-
headers = {
|
|
21
|
+
headers = {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"X-Promty-Collector-Version": COLLECTOR_VERSION,
|
|
24
|
+
}
|
|
20
25
|
if self.token:
|
|
21
26
|
headers["Authorization"] = f"Bearer {self.token}"
|
|
22
27
|
|
package/src/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
COLLECTOR_VERSION = "0.1.2"
|