wafer-cli 0.2.2__py3-none-any.whl → 0.2.3__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.
wafer/analytics.py ADDED
@@ -0,0 +1,307 @@
1
+ """PostHog analytics for Wafer CLI.
2
+
3
+ Tracks CLI command usage and user activity for product analytics.
4
+ Mirrors the analytics implementation in apps/wevin-extension/src/services/analytics.ts.
5
+
6
+ Usage:
7
+ from .analytics import track_command, identify_user, shutdown_analytics
8
+
9
+ # Track a command execution
10
+ track_command("evaluate", {"subcommand": "kernelbench", "outcome": "success"})
11
+
12
+ # Identify user after login
13
+ identify_user("user-id", "user@example.com")
14
+ """
15
+
16
+ import atexit
17
+ import platform
18
+ import sys
19
+ import uuid
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ # PostHog configuration - same as wevin-extension
24
+ POSTHOG_API_KEY = "phc_9eDjkY72ud9o4l1mA1Gr1dnRT1yx71rP3XY9z66teFh"
25
+ POSTHOG_HOST = "https://us.i.posthog.com"
26
+
27
+ # Anonymous ID storage
28
+ ANONYMOUS_ID_FILE = Path.home() / ".wafer" / ".analytics_id"
29
+
30
+ # Global state
31
+ _posthog_client: Any = None
32
+ _distinct_id: str | None = None
33
+ _initialized: bool = False
34
+
35
+
36
+ def _get_anonymous_id() -> str:
37
+ """Get or create anonymous ID for users who aren't logged in."""
38
+ if ANONYMOUS_ID_FILE.exists():
39
+ return ANONYMOUS_ID_FILE.read_text().strip()
40
+
41
+ # Generate new anonymous ID
42
+ anonymous_id = f"anon_{uuid.uuid4().hex}"
43
+ ANONYMOUS_ID_FILE.parent.mkdir(parents=True, exist_ok=True)
44
+ ANONYMOUS_ID_FILE.write_text(anonymous_id)
45
+ return anonymous_id
46
+
47
+
48
+ def _get_user_id_from_credentials() -> tuple[str | None, str | None]:
49
+ """Get user ID and email from stored credentials.
50
+
51
+ Returns:
52
+ Tuple of (user_id, email), both may be None if not logged in.
53
+ """
54
+ # Import here to avoid circular imports
55
+ from .auth import load_credentials, verify_token
56
+
57
+ creds = load_credentials()
58
+ if not creds:
59
+ return None, None
60
+
61
+ # Try to get user info from token
62
+ try:
63
+ user_info = verify_token(creds.access_token)
64
+ return user_info.user_id, user_info.email or creds.email
65
+ except Exception:
66
+ # Token verification failed, use email from credentials if available
67
+ return None, creds.email
68
+
69
+
70
+ def _is_analytics_enabled() -> bool:
71
+ """Check if analytics is enabled via preferences.
72
+
73
+ Returns True by default, respects user preference in config.
74
+ """
75
+ from .global_config import get_preferences
76
+
77
+ try:
78
+ prefs = get_preferences()
79
+ return getattr(prefs, "analytics_enabled", True)
80
+ except Exception:
81
+ # Default to enabled if we can't read preferences
82
+ return True
83
+
84
+
85
+ def init_analytics() -> bool:
86
+ """Initialize PostHog client.
87
+
88
+ Returns:
89
+ True if initialization succeeded, False otherwise.
90
+ """
91
+ global _posthog_client, _distinct_id, _initialized
92
+
93
+ if _initialized:
94
+ return _posthog_client is not None
95
+
96
+ _initialized = True
97
+
98
+ # Check if analytics is enabled
99
+ if not _is_analytics_enabled():
100
+ return False
101
+
102
+ try:
103
+ from posthog import Posthog
104
+
105
+ _posthog_client = Posthog(
106
+ api_key=POSTHOG_API_KEY,
107
+ host=POSTHOG_HOST,
108
+ # Flush immediately for CLI - commands are short-lived
109
+ flush_at=1,
110
+ flush_interval=1,
111
+ # Disable debug logging
112
+ debug=False,
113
+ )
114
+
115
+ # Set up distinct ID - prefer authenticated user, fall back to anonymous
116
+ user_id, email = _get_user_id_from_credentials()
117
+ if user_id:
118
+ _distinct_id = user_id
119
+ # Identify the user with their email
120
+ if email:
121
+ _posthog_client.identify(
122
+ distinct_id=user_id,
123
+ properties={
124
+ "email": email,
125
+ "auth_provider": "github",
126
+ },
127
+ )
128
+ else:
129
+ _distinct_id = _get_anonymous_id()
130
+
131
+ # Register shutdown handler to flush events
132
+ atexit.register(shutdown_analytics)
133
+
134
+ return True
135
+
136
+ except ImportError:
137
+ # PostHog not installed - analytics disabled
138
+ return False
139
+ except Exception:
140
+ # Any other error - fail silently, don't break CLI
141
+ return False
142
+
143
+
144
+ def shutdown_analytics() -> None:
145
+ """Shutdown PostHog client and flush pending events."""
146
+ global _posthog_client
147
+
148
+ if _posthog_client is not None:
149
+ try:
150
+ _posthog_client.flush()
151
+ _posthog_client.shutdown()
152
+ except Exception:
153
+ pass # Fail silently on shutdown
154
+ _posthog_client = None
155
+
156
+
157
+ def identify_user(user_id: str, email: str | None = None) -> None:
158
+ """Identify a user after login.
159
+
160
+ Args:
161
+ user_id: Supabase user ID
162
+ email: User's email address
163
+ """
164
+ global _distinct_id
165
+
166
+ if not init_analytics():
167
+ return
168
+
169
+ if _posthog_client is None:
170
+ return
171
+
172
+ _distinct_id = user_id
173
+
174
+ try:
175
+ properties: dict[str, Any] = {"auth_provider": "github"}
176
+ if email:
177
+ properties["email"] = email
178
+
179
+ _posthog_client.identify(
180
+ distinct_id=user_id,
181
+ properties=properties,
182
+ )
183
+ _posthog_client.flush()
184
+ except Exception:
185
+ pass # Fail silently
186
+
187
+
188
+ def reset_user_identity() -> None:
189
+ """Reset user identity after logout."""
190
+ global _distinct_id
191
+
192
+ _distinct_id = _get_anonymous_id()
193
+
194
+
195
+ def get_distinct_id() -> str:
196
+ """Get current distinct ID for tracking."""
197
+ global _distinct_id
198
+
199
+ if _distinct_id is None:
200
+ user_id, _ = _get_user_id_from_credentials()
201
+ _distinct_id = user_id or _get_anonymous_id()
202
+
203
+ return _distinct_id
204
+
205
+
206
+ def _get_cli_version() -> str:
207
+ """Get CLI version from package metadata."""
208
+ try:
209
+ from importlib.metadata import version
210
+
211
+ return version("wafer-cli")
212
+ except Exception:
213
+ return "unknown"
214
+
215
+
216
+ def _get_base_properties() -> dict[str, Any]:
217
+ """Get base properties included with all events."""
218
+ return {
219
+ "platform": "cli",
220
+ "tool_id": "cli",
221
+ "cli_version": _get_cli_version(),
222
+ "os": platform.system().lower(),
223
+ "os_version": platform.release(),
224
+ "python_version": platform.python_version(),
225
+ }
226
+
227
+
228
+ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> None:
229
+ """Track a generic event.
230
+
231
+ Args:
232
+ event_name: Name of the event to track
233
+ properties: Additional properties to include
234
+ """
235
+ if not init_analytics():
236
+ return
237
+
238
+ if _posthog_client is None:
239
+ return
240
+
241
+ try:
242
+ event_properties = _get_base_properties()
243
+ if properties:
244
+ event_properties.update(properties)
245
+
246
+ _posthog_client.capture(
247
+ distinct_id=get_distinct_id(),
248
+ event=event_name,
249
+ properties=event_properties,
250
+ )
251
+ except Exception:
252
+ pass # Fail silently
253
+
254
+
255
+ def track_command(
256
+ command: str,
257
+ subcommand: str | None = None,
258
+ outcome: str = "success",
259
+ duration_ms: int | None = None,
260
+ properties: dict[str, Any] | None = None,
261
+ ) -> None:
262
+ """Track a CLI command execution.
263
+
264
+ This event counts towards DAU in the internal dashboard.
265
+
266
+ Args:
267
+ command: The main command name (e.g., "evaluate", "agent")
268
+ subcommand: Optional subcommand (e.g., "kernelbench")
269
+ outcome: "success" or "error"
270
+ duration_ms: Command execution time in milliseconds
271
+ properties: Additional properties to include
272
+ """
273
+ event_properties: dict[str, Any] = {
274
+ "command": command,
275
+ "outcome": outcome,
276
+ }
277
+
278
+ if subcommand:
279
+ event_properties["subcommand"] = subcommand
280
+
281
+ if duration_ms is not None:
282
+ event_properties["duration_ms"] = duration_ms
283
+
284
+ if properties:
285
+ event_properties.update(properties)
286
+
287
+ track_event("cli_command_executed", event_properties)
288
+
289
+
290
+ def track_login(user_id: str, email: str | None = None) -> None:
291
+ """Track user login event.
292
+
293
+ Args:
294
+ user_id: Supabase user ID
295
+ email: User's email address
296
+ """
297
+ # First identify the user
298
+ identify_user(user_id, email)
299
+
300
+ # Then track the login event
301
+ track_event("cli_user_signed_in", {"user_id": user_id})
302
+
303
+
304
+ def track_logout() -> None:
305
+ """Track user logout event."""
306
+ track_event("cli_user_signed_out")
307
+ reset_user_identity()
wafer/auth.py CHANGED
@@ -287,7 +287,7 @@ if (accessToken) {
287
287
  self.end_headers()
288
288
 
289
289
 
290
- def browser_login(timeout: int = 120) -> tuple[str, str | None]:
290
+ def browser_login(timeout: int = 120, port: int | None = None) -> tuple[str, str | None]:
291
291
  """Open browser for GitHub OAuth and return tokens.
292
292
 
293
293
  Starts a local HTTP server, opens browser to Supabase OAuth,
@@ -295,6 +295,7 @@ def browser_login(timeout: int = 120) -> tuple[str, str | None]:
295
295
 
296
296
  Args:
297
297
  timeout: Seconds to wait for callback (default 120)
298
+ port: Port for callback server. If None, finds a free port (default None)
298
299
 
299
300
  Returns:
300
301
  Tuple of (access_token, refresh_token). refresh_token may be None.
@@ -303,7 +304,8 @@ def browser_login(timeout: int = 120) -> tuple[str, str | None]:
303
304
  TimeoutError: If no callback received within timeout
304
305
  RuntimeError: If OAuth flow failed
305
306
  """
306
- port = _find_free_port()
307
+ if port is None:
308
+ port = _find_free_port()
307
309
  redirect_uri = f"http://localhost:{port}/callback"
308
310
  supabase_url = get_supabase_url()
309
311
 
wafer/cli.py CHANGED
@@ -18,9 +18,11 @@ Setup:
18
18
  config CLI configuration and local GPU targets
19
19
  """
20
20
 
21
+ import atexit
21
22
  import json
22
23
  import os
23
24
  import sys
25
+ import time
24
26
  from pathlib import Path
25
27
 
26
28
  import trio
@@ -34,6 +36,98 @@ app = typer.Typer(
34
36
  no_args_is_help=True,
35
37
  )
36
38
 
39
+ # =============================================================================
40
+ # Analytics tracking
41
+ # =============================================================================
42
+
43
+ # Track command start time for duration calculation
44
+ _command_start_time: float | None = None
45
+ # Track command outcome (defaults to failure, set to success on clean exit)
46
+ _command_outcome: str = "failure"
47
+
48
+
49
+ def _get_command_path(ctx: typer.Context) -> tuple[str, str | None]:
50
+ """Extract command and subcommand from Typer context.
51
+
52
+ Returns:
53
+ Tuple of (command, subcommand). subcommand may be None.
54
+ """
55
+ # Build command path from invoked subcommand chain
56
+ invoked = ctx.invoked_subcommand
57
+ info_name = ctx.info_name or ""
58
+
59
+ # Get parent command if exists
60
+ parent_cmd = None
61
+ if ctx.parent and ctx.parent.info_name and ctx.parent.info_name != "wafer":
62
+ parent_cmd = ctx.parent.info_name
63
+
64
+ if parent_cmd:
65
+ return parent_cmd, info_name
66
+ return info_name or "unknown", invoked
67
+
68
+
69
+ def _mark_command_success() -> None:
70
+ """Mark the current command as successful.
71
+
72
+ Call this at the end of successful command execution.
73
+ Commands that raise typer.Exit(1) or exceptions will remain marked as failures.
74
+ """
75
+ global _command_outcome
76
+ _command_outcome = "success"
77
+
78
+
79
+ @app.callback()
80
+ def main_callback(ctx: typer.Context) -> None:
81
+ """Initialize analytics and track command execution."""
82
+ global _command_start_time, _command_outcome
83
+ _command_start_time = time.time()
84
+ _command_outcome = "success" # Default to success, mark failure on exceptions
85
+
86
+ # Initialize analytics (lazy import to avoid slowing down --help)
87
+ from . import analytics
88
+
89
+ analytics.init_analytics()
90
+
91
+ # Install exception hook to catch SystemExit and mark failures
92
+ original_excepthook = sys.excepthook
93
+
94
+ def custom_excepthook(exc_type, exc_value, exc_traceback):
95
+ global _command_outcome
96
+ # Mark as failure if SystemExit with non-zero code, or any other exception
97
+ if exc_type is SystemExit:
98
+ exit_code = exc_value.code if hasattr(exc_value, 'code') else 1
99
+ if exit_code != 0 and exit_code is not None:
100
+ _command_outcome = "failure"
101
+ else:
102
+ _command_outcome = "failure"
103
+ # Call original excepthook
104
+ original_excepthook(exc_type, exc_value, exc_traceback)
105
+
106
+ sys.excepthook = custom_excepthook
107
+
108
+ # Register tracking at exit to capture command outcome
109
+ def track_on_exit() -> None:
110
+ command, subcommand = _get_command_path(ctx)
111
+
112
+ # Skip tracking for --help and --version
113
+ if ctx.resilient_parsing:
114
+ return
115
+
116
+ # Calculate duration
117
+ duration_ms = None
118
+ if _command_start_time is not None:
119
+ duration_ms = int((time.time() - _command_start_time) * 1000)
120
+
121
+ # Track the command execution with the recorded outcome
122
+ analytics.track_command(
123
+ command=command,
124
+ subcommand=subcommand,
125
+ outcome=_command_outcome,
126
+ duration_ms=duration_ms,
127
+ )
128
+
129
+ atexit.register(track_on_exit)
130
+
37
131
 
38
132
  # =============================================================================
39
133
  # Autocompletion helpers
@@ -1945,17 +2039,31 @@ def login(
1945
2039
  token: str | None = typer.Option(
1946
2040
  None, "--token", "-t", help="Access token (skip browser OAuth)"
1947
2041
  ),
2042
+ port: int | None = typer.Option(
2043
+ None, "--port", "-p", help="Port for OAuth callback server (default: 8765 for SSH, random for local)"
2044
+ ),
1948
2045
  ) -> None:
1949
2046
  """Authenticate CLI with wafer-api via GitHub OAuth.
1950
2047
 
1951
2048
  Opens browser for GitHub authentication. Use --token to skip browser.
1952
2049
  Uses the API environment from config (see 'wafer config show').
1953
2050
 
2051
+ SSH Users:
2052
+ - Automatically uses port 8765 (just set up port forwarding once)
2053
+ - On local machine: ssh -L 8765:localhost:8765 user@host
2054
+ - On remote machine: wafer login
2055
+ - Browser opens locally, redirect works through tunnel
2056
+
2057
+ Manual token option:
2058
+ - Visit auth.wafer.ai, authenticate, copy token from URL
2059
+ - Run: wafer login --token <paste-token>
2060
+
1954
2061
  Examples:
1955
- wafer login # opens browser for GitHub OAuth
1956
- wafer login --token xyz # use existing token
2062
+ wafer login # auto-detects SSH, uses appropriate port
2063
+ wafer login --port 9000 # override port
2064
+ wafer login --token xyz # manual token (no browser)
1957
2065
 
1958
- # To login to a different environment:
2066
+ # Change environment:
1959
2067
  wafer config set api.environment staging
1960
2068
  wafer login
1961
2069
  """
@@ -1971,11 +2079,21 @@ def login(
1971
2079
  typer.echo(f"Auth: {get_supabase_url()}")
1972
2080
  typer.echo("")
1973
2081
 
2082
+ # Auto-detect SSH and use fixed port
2083
+ if port is None:
2084
+ is_ssh = bool(os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_CLIENT"))
2085
+ if is_ssh:
2086
+ port = 8765
2087
+ typer.echo("🔒 SSH session detected - using port 8765 for OAuth callback")
2088
+ typer.echo(" Make sure you have port forwarding set up:")
2089
+ typer.echo(" ssh -L 8765:localhost:8765 user@host")
2090
+ typer.echo("")
2091
+
1974
2092
  # Browser OAuth if no token provided
1975
2093
  refresh_token = None
1976
2094
  if token is None:
1977
2095
  try:
1978
- token, refresh_token = browser_login()
2096
+ token, refresh_token = browser_login(port=port)
1979
2097
  except TimeoutError as e:
1980
2098
  typer.echo(f"Error: {e}", err=True)
1981
2099
  raise typer.Exit(1) from None
@@ -2009,6 +2127,11 @@ def login(
2009
2127
  # Save credentials (with refresh token if available)
2010
2128
  save_credentials(token, refresh_token, user_info.email)
2011
2129
 
2130
+ # Track login event with analytics
2131
+ from . import analytics
2132
+
2133
+ analytics.track_login(user_info.user_id, user_info.email)
2134
+
2012
2135
  if user_info.email:
2013
2136
  typer.echo(f"Logged in as {user_info.email}")
2014
2137
  else:
@@ -2021,6 +2144,13 @@ def logout() -> None:
2021
2144
  """Remove stored credentials."""
2022
2145
  from .auth import clear_credentials
2023
2146
 
2147
+ from . import analytics
2148
+
2149
+ # Track logout event first (while credentials still exist for user identification)
2150
+ # Note: track_logout() handles the case where user is not logged in
2151
+ analytics.track_logout()
2152
+
2153
+ # Clear credentials and report result
2024
2154
  if clear_credentials():
2025
2155
  typer.echo("Logged out. Credentials removed.")
2026
2156
  else:
@@ -4298,6 +4428,29 @@ autotuner_app = typer.Typer(help="Hyperparameter sweep for performance engineeri
4298
4428
  app.add_typer(autotuner_app, name="autotuner", hidden=True)
4299
4429
 
4300
4430
 
4431
+ def _setup_wafer_core_env() -> None:
4432
+ """Set environment variables for wafer-core to use.
4433
+
4434
+ Call this before using any wafer-core functions that need API access.
4435
+
4436
+ Respects explicit environment variable overrides:
4437
+ - WAFER_API_URL: If already set, uses that instead of config
4438
+ - WAFER_AUTH_TOKEN: If already set, uses that instead of cached token
4439
+ """
4440
+ from .global_config import get_api_url
4441
+ from .auth import get_valid_token
4442
+
4443
+ # Set API URL (get_api_url already respects WAFER_API_URL env var)
4444
+ os.environ["WAFER_API_URL"] = get_api_url()
4445
+
4446
+ # Only set auth token if not explicitly provided in environment
4447
+ # This allows CI/service accounts to override with their own tokens
4448
+ if "WAFER_AUTH_TOKEN" not in os.environ:
4449
+ token = get_valid_token()
4450
+ if token:
4451
+ os.environ["WAFER_AUTH_TOKEN"] = token
4452
+
4453
+
4301
4454
  @autotuner_app.command("list")
4302
4455
  def autotuner_list(
4303
4456
  show_all: bool = typer.Option(
@@ -4313,6 +4466,7 @@ def autotuner_list(
4313
4466
  wafer autotuner list
4314
4467
  wafer autotuner list --all
4315
4468
  """
4469
+ _setup_wafer_core_env()
4316
4470
  from .autotuner import list_command
4317
4471
 
4318
4472
  try:
@@ -4353,6 +4507,7 @@ def autotuner_delete(
4353
4507
  wafer autotuner delete --all --status pending
4354
4508
  wafer autotuner delete --all --status failed --yes
4355
4509
  """
4510
+ _setup_wafer_core_env()
4356
4511
  from .autotuner import delete_all_command, delete_command
4357
4512
 
4358
4513
  # Validate arguments
@@ -4419,6 +4574,7 @@ def autotuner_run(
4419
4574
  wafer autotuner run --resume <sweep-id>
4420
4575
  wafer autotuner run --resume <sweep-id> --parallel 8
4421
4576
  """
4577
+ _setup_wafer_core_env()
4422
4578
  from .autotuner import run_sweep_command
4423
4579
 
4424
4580
  # Validate arguments
@@ -4583,8 +4739,23 @@ def capture_command( # noqa: PLR0915
4583
4739
  wafer capture grid-search "python train.py --lr {LR} --bs {BS}" --sweep "LR=0.001,0.01,0.1" --sweep "BS=16,32"
4584
4740
  """
4585
4741
  import itertools
4742
+ import os
4586
4743
  import tomllib
4587
4744
 
4745
+ from .global_config import get_api_url
4746
+ from .auth import get_valid_token
4747
+
4748
+ # Set environment variables for wafer-core BEFORE importing it
4749
+ # wafer-core backend.py reads WAFER_API_URL and WAFER_AUTH_TOKEN from env
4750
+ os.environ["WAFER_API_URL"] = get_api_url()
4751
+
4752
+ # Only set auth token if not explicitly provided in environment
4753
+ # This allows CI/service accounts to override with their own tokens
4754
+ if "WAFER_AUTH_TOKEN" not in os.environ:
4755
+ token = get_valid_token()
4756
+ if token:
4757
+ os.environ["WAFER_AUTH_TOKEN"] = token
4758
+
4588
4759
  import trio
4589
4760
  from wafer_core.tools.capture_tool import ( # pragma: no cover
4590
4761
  CaptureConfig,
@@ -4774,6 +4945,20 @@ def capture_list_command(
4774
4945
  # Pagination
4775
4946
  wafer capture-list --limit 20 --offset 20
4776
4947
  """
4948
+ import os
4949
+
4950
+ from .global_config import get_api_url
4951
+ from .auth import get_valid_token
4952
+
4953
+ # Set environment variables for wafer-core BEFORE importing it
4954
+ os.environ["WAFER_API_URL"] = get_api_url()
4955
+
4956
+ # Only set auth token if not explicitly provided in environment
4957
+ # This allows CI/service accounts to override with their own tokens
4958
+ if "WAFER_AUTH_TOKEN" not in os.environ:
4959
+ token = get_valid_token()
4960
+ if token:
4961
+ os.environ["WAFER_AUTH_TOKEN"] = token
4777
4962
 
4778
4963
  import trio
4779
4964
  from wafer_core.utils.backend import list_captures # pragma: no cover
wafer/global_config.py CHANGED
@@ -67,9 +67,12 @@ class Preferences:
67
67
 
68
68
  mode: "implicit" (default) = quiet output, use -v for status messages
69
69
  "explicit" = verbose output, shows [wafer] status messages
70
+ analytics_enabled: True (default) = send anonymous usage analytics to PostHog
71
+ False = disable all analytics tracking
70
72
  """
71
73
 
72
74
  mode: Literal["explicit", "implicit"] = "implicit"
75
+ analytics_enabled: bool = True
73
76
 
74
77
 
75
78
  @dataclass(frozen=True)
@@ -148,7 +151,9 @@ def _parse_config_file(path: Path) -> GlobalConfig:
148
151
  pref_data = data.get("preferences", {})
149
152
  mode = pref_data.get("mode", "explicit")
150
153
  assert mode in ("explicit", "implicit"), f"mode must be 'explicit' or 'implicit', got '{mode}'"
151
- preferences = Preferences(mode=mode)
154
+ analytics_enabled = pref_data.get("analytics_enabled", True)
155
+ assert isinstance(analytics_enabled, bool), f"analytics_enabled must be true or false, got '{analytics_enabled}'"
156
+ preferences = Preferences(mode=mode, analytics_enabled=analytics_enabled)
152
157
 
153
158
  # Parse defaults
154
159
  defaults_data = data.get("defaults", {})
@@ -301,10 +306,16 @@ def save_global_config(config: GlobalConfig) -> None:
301
306
  lines.append(f"{key} = {value}")
302
307
  lines.append("")
303
308
 
304
- # Preferences section (only if non-default)
309
+ # Preferences section (only if non-default values)
310
+ pref_lines = []
305
311
  if config.preferences.mode != "implicit":
312
+ pref_lines.append(f'mode = "{config.preferences.mode}"')
313
+ if not config.preferences.analytics_enabled:
314
+ pref_lines.append("analytics_enabled = false")
315
+
316
+ if pref_lines:
306
317
  lines.append("[preferences]")
307
- lines.append(f'mode = "{config.preferences.mode}"')
318
+ lines.extend(pref_lines)
308
319
  lines.append("")
309
320
 
310
321
  # Defaults section (only if non-default values)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wafer-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: CLI tool for running commands on remote GPUs and GPU kernel optimization agent
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: typer>=0.12.0
@@ -8,6 +8,7 @@ Requires-Dist: trio>=0.24.0
8
8
  Requires-Dist: trio-asyncio>=0.15.0
9
9
  Requires-Dist: wafer-core>=0.1.0
10
10
  Requires-Dist: perfetto>=0.16.0
11
+ Requires-Dist: posthog>=3.0.0
11
12
  Provides-Extra: dev
12
13
  Requires-Dist: pytest>=8.0.0; extra == "dev"
13
14
  Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
@@ -1,14 +1,15 @@
1
1
  wafer/GUIDE.md,sha256=Z_jsSgHAS6bFa83VKhG9jxjUK1XpLjR1fEIKapDa_6g,3195
2
2
  wafer/__init__.py,sha256=kBM_ONCpU6UUMBOH8Tmg4A88sNFnbaD59o61cJs-uYM,90
3
+ wafer/analytics.py,sha256=Xxw3bbY3XLgedSJPwzIOBJIjyycIiornWCpjoWbTKYU,8190
3
4
  wafer/api_client.py,sha256=cPULiTxqOAYYSfDTNJgd-6Pqrt3IM4Gm9903U7yGIwY,6163
4
- wafer/auth.py,sha256=3ExbDx5amwX4JiW3Ee9Vg0f0ohsS-8bqeqMIFPPE4dw,10571
5
+ wafer/auth.py,sha256=ZLsXZ73GDLD8GL7Rij1ELtuLqyJ5EU_uPBUMPVKwExA,10703
5
6
  wafer/autotuner.py,sha256=6gH0Ho7T58EFerMQcHQxshWe3DF4qU7fb5xthAh5SPM,44364
6
7
  wafer/billing.py,sha256=jbLB2lI4_9f2KD8uEFDi_ixLlowe5hasC0TIZJyIXRg,7163
7
- wafer/cli.py,sha256=TEHxiG9e3T_eCFlwfh2FOetzGwdc1YkI6_5QWh8m5OY,178115
8
+ wafer/cli.py,sha256=kscK-JBiAu0H7roMdcaZ_lT7xlgR6mUJ1nBruidUdsE,184745
8
9
  wafer/config.py,sha256=h5Eo9_yfWqWGoPNdVQikI9GoZVUeysunSYiixf1mKcw,3411
9
10
  wafer/corpus.py,sha256=yTF3UA5bOa8BII2fmcXf-3WsIsM5DX4etysv0AzVknE,8912
10
11
  wafer/evaluate.py,sha256=7E1BqVl0um7kfU2_4FkXZrNc-PQJIHe09G1-L07PXmg,114971
11
- wafer/global_config.py,sha256=nQOCNjLPeXdFb0WPRpKXukXAisBNjTBw2Va6bTQGNro,10757
12
+ wafer/global_config.py,sha256=fhaR_RU3ufMksDmOohH1OLeQ0JT0SDW1hEip_zaP75k,11345
12
13
  wafer/gpu_run.py,sha256=gUbzMsMPsw3UHcn00bI-zTSHrU8c2FEpDvbcsczlDPo,9557
13
14
  wafer/inference.py,sha256=tZCO5i05FKY27ewis3CSBHFBeFbXY3xwj0DSjdoMY9s,4314
14
15
  wafer/ncu_analyze.py,sha256=rAWzKQRZEY6E_CL3gAWUaW3uZ4kvQVZskVCPDpsFJuE,24633
@@ -25,8 +26,8 @@ wafer/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
26
  wafer/templates/ask_docs.py,sha256=Lxs-faz9v5m4Qa4NjF2X_lE8KwM9ES9MNJkxo7ep56o,2256
26
27
  wafer/templates/optimize_kernel.py,sha256=u6AL7Q3uttqlnBLzcoFdsiPq5lV2TV3bgqwCYYlK9gk,2357
27
28
  wafer/templates/trace_analyze.py,sha256=XE1VqzVkIUsZbXF8EzQdDYgg-AZEYAOFpr6B_vnRELc,2880
28
- wafer_cli-0.2.2.dist-info/METADATA,sha256=HKyOvF4UTxmC2_F2PoRLDHg27q-4qeQzFn9fk2-qblc,529
29
- wafer_cli-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- wafer_cli-0.2.2.dist-info/entry_points.txt,sha256=WqB7hB__WhtPY8y1cO2sZiUz7fCq6Ik-usAigpeFvWE,41
31
- wafer_cli-0.2.2.dist-info/top_level.txt,sha256=2MK1IVMWfpLL8BZCQ3E9aG6L6L666gSA_teYlwan4fs,6
32
- wafer_cli-0.2.2.dist-info/RECORD,,
29
+ wafer_cli-0.2.3.dist-info/METADATA,sha256=MKzRmMwc1zhKUVeu_NE4h5_17EFnX1bjLE4Jcox0AAk,559
30
+ wafer_cli-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ wafer_cli-0.2.3.dist-info/entry_points.txt,sha256=WqB7hB__WhtPY8y1cO2sZiUz7fCq6Ik-usAigpeFvWE,41
32
+ wafer_cli-0.2.3.dist-info/top_level.txt,sha256=2MK1IVMWfpLL8BZCQ3E9aG6L6L666gSA_teYlwan4fs,6
33
+ wafer_cli-0.2.3.dist-info/RECORD,,