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 CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "promty-collector",
3
- "version": "0.1.0",
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:
@@ -25,6 +25,8 @@ RUNTIME_MODULES: tuple[str, ...] = (
25
25
  "runtime_install.py",
26
26
  "sequence.py",
27
27
  "session_index.py",
28
+ "updater.py",
29
+ "version.py",
28
30
  )
29
31
  RUNTIME_PACKAGES: tuple[str, ...] = ("adapters", "uploader")
30
32
 
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
@@ -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 = {"Content-Type": "application/json"}
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"