golf-mcp 0.1.19__py3-none-any.whl → 0.2.0__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.

Potentially problematic release.


This version of golf-mcp might be problematic. Click here for more details.

Files changed (123) hide show
  1. golf/__init__.py +9 -1
  2. golf/_endpoints.py +6 -0
  3. golf/_endpoints_fallback.py +10 -0
  4. golf/auth/__init__.py +188 -84
  5. golf/auth/api_key.py +6 -14
  6. golf/auth/factory.py +333 -0
  7. golf/auth/helpers.py +12 -42
  8. golf/auth/providers.py +396 -0
  9. golf/auth/registry.py +256 -0
  10. golf/cli/branding.py +192 -0
  11. golf/cli/main.py +28 -69
  12. golf/commands/__init__.py +2 -0
  13. golf/commands/build.py +4 -7
  14. golf/commands/init.py +30 -53
  15. golf/commands/run.py +50 -20
  16. golf/core/builder.py +356 -412
  17. golf/core/builder_auth.py +63 -144
  18. golf/core/builder_telemetry.py +26 -3
  19. golf/core/config.py +38 -59
  20. golf/core/parser.py +132 -139
  21. golf/core/platform.py +12 -10
  22. golf/core/telemetry.py +11 -19
  23. golf/core/transformer.py +38 -15
  24. golf/examples/__pycache__/__init__.cpython-311.pyc +0 -0
  25. golf/examples/basic/.coverage +0 -0
  26. golf/examples/basic/.env.example +8 -4
  27. golf/examples/basic/README.md +117 -45
  28. golf/examples/basic/__pycache__/auth.cpython-311.pyc +0 -0
  29. golf/examples/basic/auth.py +76 -0
  30. golf/examples/basic/golf.json +2 -5
  31. golf/examples/basic/htmlcov/.gitignore +2 -0
  32. golf/examples/basic/htmlcov/class_index.html +547 -0
  33. golf/examples/basic/htmlcov/coverage_html_cb_6fb7b396.js +733 -0
  34. golf/examples/basic/htmlcov/favicon_32_cb_58284776.png +0 -0
  35. golf/examples/basic/htmlcov/function_index.html +2091 -0
  36. golf/examples/basic/htmlcov/index.html +349 -0
  37. golf/examples/basic/htmlcov/keybd_closed_cb_ce680311.png +0 -0
  38. golf/examples/basic/htmlcov/status.json +1 -0
  39. golf/examples/basic/htmlcov/style_cb_8e611ae1.css +337 -0
  40. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496___init___py.html +323 -0
  41. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_api_key_py.html +170 -0
  42. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_factory_py.html +430 -0
  43. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_helpers_py.html +288 -0
  44. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_providers_py.html +493 -0
  45. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_registry_py.html +353 -0
  46. golf/examples/basic/htmlcov/z_3ec3b3f490dc0950___init___py.html +120 -0
  47. golf/examples/basic/htmlcov/z_3ec3b3f490dc0950_instrumentation_py.html +1535 -0
  48. golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db___init___py.html +98 -0
  49. golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_branding_py.html +289 -0
  50. golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_main_py.html +476 -0
  51. golf/examples/basic/htmlcov/z_5a6c4e6bcc86fb2f___init___py.html +97 -0
  52. golf/examples/basic/htmlcov/z_6cadab9ec0df475d___init___py.html +102 -0
  53. golf/examples/basic/htmlcov/z_6cadab9ec0df475d_build_py.html +178 -0
  54. golf/examples/basic/htmlcov/z_6cadab9ec0df475d_init_py.html +387 -0
  55. golf/examples/basic/htmlcov/z_6cadab9ec0df475d_run_py.html +222 -0
  56. golf/examples/basic/htmlcov/z_6fcdee0582ba84e4___init___py.html +106 -0
  57. golf/examples/basic/htmlcov/z_6fcdee0582ba84e4__endpoints_fallback_py.html +107 -0
  58. golf/examples/basic/htmlcov/z_7ba499ed22986217___init___py.html +98 -0
  59. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_auth_py.html +306 -0
  60. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_metrics_py.html +329 -0
  61. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_py.html +1471 -0
  62. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_telemetry_py.html +186 -0
  63. golf/examples/basic/htmlcov/z_7ba499ed22986217_config_py.html +315 -0
  64. golf/examples/basic/htmlcov/z_7ba499ed22986217_parser_py.html +1149 -0
  65. golf/examples/basic/htmlcov/z_7ba499ed22986217_platform_py.html +279 -0
  66. golf/examples/basic/htmlcov/z_7ba499ed22986217_telemetry_py.html +589 -0
  67. golf/examples/basic/htmlcov/z_7ba499ed22986217_transformer_py.html +286 -0
  68. golf/examples/basic/htmlcov/z_7d7da37693a43688___init___py.html +107 -0
  69. golf/examples/basic/htmlcov/z_7d7da37693a43688_collector_py.html +417 -0
  70. golf/examples/basic/htmlcov/z_7d7da37693a43688_registry_py.html +109 -0
  71. golf/examples/basic/htmlcov/z_abe733142b40ad4e___init___py.html +109 -0
  72. golf/examples/basic/htmlcov/z_abe733142b40ad4e_context_py.html +150 -0
  73. golf/examples/basic/htmlcov/z_abe733142b40ad4e_elicitation_py.html +267 -0
  74. golf/examples/basic/htmlcov/z_abe733142b40ad4e_sampling_py.html +318 -0
  75. golf/examples/basic/prompts/__pycache__/welcome.cpython-311.pyc +0 -0
  76. golf/examples/basic/prompts/welcome.py +3 -5
  77. golf/examples/basic/resources/__pycache__/current_time.cpython-311.pyc +0 -0
  78. golf/examples/basic/resources/__pycache__/info.cpython-311.pyc +0 -0
  79. golf/examples/basic/resources/current_time.py +5 -13
  80. golf/examples/basic/resources/weather/__pycache__/common.cpython-311.pyc +0 -0
  81. golf/examples/basic/resources/weather/__pycache__/current.cpython-311.pyc +0 -0
  82. golf/examples/basic/resources/weather/__pycache__/forecast.cpython-311.pyc +0 -0
  83. golf/examples/basic/resources/weather/city.py +46 -0
  84. golf/examples/basic/resources/weather/common.py +4 -11
  85. golf/examples/basic/resources/weather/current.py +5 -5
  86. golf/examples/basic/resources/weather/forecast.py +5 -5
  87. golf/examples/basic/tools/__pycache__/calculator.cpython-311.pyc +0 -0
  88. golf/examples/basic/tools/calculator.py +94 -0
  89. golf/examples/basic/tools/say/__pycache__/hello.cpython-311.pyc +0 -0
  90. golf/examples/basic/tools/say/hello.py +65 -0
  91. golf/metrics/collector.py +100 -19
  92. golf/telemetry/__init__.py +4 -0
  93. golf/telemetry/instrumentation.py +496 -174
  94. golf/utilities/__init__.py +12 -0
  95. golf/utilities/context.py +53 -0
  96. golf/utilities/elicitation.py +170 -0
  97. golf/utilities/sampling.py +221 -0
  98. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/METADATA +56 -110
  99. golf_mcp-0.2.0.dist-info/RECORD +110 -0
  100. golf/auth/oauth.py +0 -861
  101. golf/auth/provider.py +0 -115
  102. golf/examples/api_key/.env +0 -2
  103. golf/examples/api_key/.env.example +0 -1
  104. golf/examples/api_key/README.md +0 -84
  105. golf/examples/api_key/golf.json +0 -8
  106. golf/examples/api_key/pre_build.py +0 -11
  107. golf/examples/api_key/tools/issues/create.py +0 -93
  108. golf/examples/api_key/tools/issues/list.py +0 -92
  109. golf/examples/api_key/tools/repos/list.py +0 -111
  110. golf/examples/api_key/tools/search/code.py +0 -106
  111. golf/examples/api_key/tools/users/get.py +0 -82
  112. golf/examples/basic/.env +0 -5
  113. golf/examples/basic/pre_build.py +0 -28
  114. golf/examples/basic/tools/github_user.py +0 -65
  115. golf/examples/basic/tools/hello.py +0 -34
  116. golf/examples/basic/tools/payments/charge.py +0 -70
  117. golf/examples/basic/tools/payments/common.py +0 -36
  118. golf/examples/basic/tools/payments/refund.py +0 -61
  119. golf_mcp-0.1.19.dist-info/RECORD +0 -60
  120. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/WHEEL +0 -0
  121. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
  122. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  123. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/top_level.txt +0 -0
golf/core/parser.py CHANGED
@@ -58,9 +58,7 @@ class AstParser:
58
58
 
59
59
  for file_path in directory.glob("**/*.py"):
60
60
  # Skip __pycache__ and other hidden directories
61
- if "__pycache__" in file_path.parts or any(
62
- part.startswith(".") for part in file_path.parts
63
- ):
61
+ if "__pycache__" in file_path.parts or any(part.startswith(".") for part in file_path.parts):
64
62
  continue
65
63
 
66
64
  try:
@@ -68,9 +66,7 @@ class AstParser:
68
66
  components.extend(file_components)
69
67
  except Exception as e:
70
68
  relative_path = file_path.relative_to(self.project_root)
71
- console.print(
72
- f"[bold red]Error parsing {relative_path}:[/bold red] {e}"
73
- )
69
+ console.print(f"[bold red]Error parsing {relative_path}:[/bold red] {e}")
74
70
 
75
71
  return components
76
72
 
@@ -123,10 +119,9 @@ class AstParser:
123
119
  for node in tree.body:
124
120
  if isinstance(node, ast.Assign):
125
121
  for target in node.targets:
126
- if isinstance(target, ast.Name) and target.id == "export":
127
- if isinstance(node.value, ast.Name):
128
- export_target = node.value.id
129
- break
122
+ if isinstance(target, ast.Name) and target.id == "export" and isinstance(node.value, ast.Name):
123
+ export_target = node.value.id
124
+ break
130
125
 
131
126
  # Find all top-level functions
132
127
  functions = []
@@ -145,9 +140,7 @@ class AstParser:
145
140
 
146
141
  # If we have an export but didn't find the target function, warn
147
142
  if export_target and not entry_function:
148
- console.print(
149
- f"[yellow]Warning: Export target '{export_target}' not found in {file_path}[/yellow]"
150
- )
143
+ console.print(f"[yellow]Warning: Export target '{export_target}' not found in {file_path}[/yellow]")
151
144
 
152
145
  # Use the export target function if found, otherwise fall back to run
153
146
  entry_function = entry_function or run_function
@@ -163,8 +156,7 @@ class AstParser:
163
156
  file_path=file_path,
164
157
  module_path=file_path.relative_to(self.project_root).as_posix(),
165
158
  docstring=module_docstring,
166
- entry_function=export_target
167
- or "run", # Store the name of the entry function
159
+ entry_function=export_target or "run", # Store the name of the entry function
168
160
  )
169
161
 
170
162
  # Process the entry function
@@ -183,9 +175,7 @@ class AstParser:
183
175
 
184
176
  # Set parent module if it's in a nested structure
185
177
  if len(rel_path.parts) > 2: # More than just "tools/file.py"
186
- parent_parts = rel_path.parts[
187
- 1:-1
188
- ] # Skip the root category and the file itself
178
+ parent_parts = rel_path.parts[1:-1] # Skip the root category and the file itself
189
179
  if parent_parts:
190
180
  component.parent_module = ".".join(parent_parts)
191
181
 
@@ -201,9 +191,7 @@ class AstParser:
201
191
  """Process the entry function to extract parameters and return type."""
202
192
  # Check for return annotation - STRICT requirement
203
193
  if func_node.returns is None:
204
- raise ValueError(
205
- f"Missing return annotation for {func_node.name} function in {file_path}"
206
- )
194
+ raise ValueError(f"Missing return annotation for {func_node.name} function in {file_path}")
207
195
 
208
196
  # Extract parameter names for basic info
209
197
  parameters = []
@@ -219,15 +207,12 @@ class AstParser:
219
207
  try:
220
208
  self._extract_schemas_at_runtime(component, file_path)
221
209
  except Exception as e:
222
- console.print(
223
- f"[yellow]Warning: Could not extract schemas from {file_path}: {e}[/yellow]"
224
- )
210
+ console.print(f"[yellow]Warning: Could not extract schemas from {file_path}: {e}[/yellow]")
225
211
  # Continue without schemas - better than failing the build
226
212
 
227
- def _extract_schemas_at_runtime(
228
- self, component: ParsedComponent, file_path: Path
229
- ) -> None:
230
- """Extract input/output schemas by importing and inspecting the actual function."""
213
+ def _extract_schemas_at_runtime(self, component: ParsedComponent, file_path: Path) -> None:
214
+ """Extract input/output schemas by importing and inspecting the
215
+ actual function."""
231
216
  import importlib.util
232
217
  import sys
233
218
 
@@ -269,7 +254,7 @@ class AstParser:
269
254
  if cleanup_path and project_root_str in sys.path:
270
255
  sys.path.remove(project_root_str)
271
256
 
272
- def _extract_input_schema(self, func) -> dict[str, Any] | None:
257
+ def _extract_input_schema(self, func: Any) -> dict[str, Any] | None:
273
258
  """Extract input schema from function signature using runtime inspection."""
274
259
  import inspect
275
260
  from typing import get_type_hints
@@ -293,9 +278,7 @@ class AstParser:
293
278
  type_hint = type_hints[param_name]
294
279
 
295
280
  # Extract schema for this parameter
296
- param_schema = self._extract_param_schema_from_hint(
297
- type_hint, param_name
298
- )
281
+ param_schema = self._extract_param_schema_from_hint(type_hint, param_name)
299
282
  if param_schema:
300
283
  # Clean the schema to remove problematic objects
301
284
  cleaned_schema = self._clean_schema(param_schema)
@@ -314,13 +297,11 @@ class AstParser:
314
297
  }
315
298
 
316
299
  except Exception as e:
317
- console.print(
318
- f"[yellow]Warning: Could not extract input schema: {e}[/yellow]"
319
- )
300
+ console.print(f"[yellow]Warning: Could not extract input schema: {e}[/yellow]")
320
301
 
321
302
  return None
322
303
 
323
- def _extract_output_schema(self, func) -> dict[str, Any] | None:
304
+ def _extract_output_schema(self, func: Any) -> dict[str, Any] | None:
324
305
  """Extract output schema from return type annotation."""
325
306
  from typing import get_type_hints
326
307
 
@@ -339,13 +320,11 @@ class AstParser:
339
320
  return self._type_to_schema(return_type)
340
321
 
341
322
  except Exception as e:
342
- console.print(
343
- f"[yellow]Warning: Could not extract output schema: {e}[/yellow]"
344
- )
323
+ console.print(f"[yellow]Warning: Could not extract output schema: {e}[/yellow]")
345
324
 
346
325
  return None
347
326
 
348
- def _extract_pydantic_model_schema(self, model_class) -> dict[str, Any]:
327
+ def _extract_pydantic_model_schema(self, model_class: Any) -> dict[str, Any]:
349
328
  """Extract schema from Pydantic model by inspecting fields directly."""
350
329
  try:
351
330
  schema = {"type": "object", "properties": {}, "required": []}
@@ -353,29 +332,19 @@ class AstParser:
353
332
  if hasattr(model_class, "model_fields"):
354
333
  for field_name, field_info in model_class.model_fields.items():
355
334
  # Extract field type
356
- field_type = (
357
- field_info.annotation
358
- if hasattr(field_info, "annotation")
359
- else None
360
- )
335
+ field_type = field_info.annotation if hasattr(field_info, "annotation") else None
361
336
  if field_type:
362
337
  field_schema = self._type_to_schema(field_type)
363
338
 
364
339
  # Add description if available
365
- if (
366
- hasattr(field_info, "description")
367
- and field_info.description
368
- ):
340
+ if hasattr(field_info, "description") and field_info.description:
369
341
  field_schema["description"] = field_info.description
370
342
 
371
343
  # Add title
372
344
  field_schema["title"] = field_name.replace("_", " ").title()
373
345
 
374
346
  # Add default if available
375
- if (
376
- hasattr(field_info, "default")
377
- and field_info.default is not None
378
- ):
347
+ if hasattr(field_info, "default") and field_info.default is not None:
379
348
  try:
380
349
  # Only add if it's JSON serializable
381
350
  import json
@@ -388,31 +357,23 @@ class AstParser:
388
357
  schema["properties"][field_name] = field_schema
389
358
 
390
359
  # Check if required
391
- if (
392
- hasattr(field_info, "is_required")
393
- and field_info.is_required()
394
- ):
360
+ if hasattr(field_info, "is_required") and field_info.is_required():
395
361
  schema["required"].append(field_name)
396
- elif (
397
- not hasattr(field_info, "default")
398
- or field_info.default is None
399
- ):
362
+ elif not hasattr(field_info, "default") or field_info.default is None:
400
363
  # Assume required if no default
401
364
  schema["required"].append(field_name)
402
365
 
403
366
  return schema
404
367
 
405
368
  except Exception as e:
406
- console.print(
407
- f"[yellow]Warning: Could not extract Pydantic model schema: {e}[/yellow]"
408
- )
369
+ console.print(f"[yellow]Warning: Could not extract Pydantic model schema: {e}[/yellow]")
409
370
  return {"type": "object"}
410
371
 
411
- def _clean_schema(self, schema) -> dict[str, Any]:
372
+ def _clean_schema(self, schema: Any) -> dict[str, Any]:
412
373
  """Clean up a schema to remove non-JSON-serializable objects."""
413
374
  import json
414
375
 
415
- def clean_object(obj):
376
+ def clean_object(obj: Any) -> Any:
416
377
  if obj is None:
417
378
  return None
418
379
  elif isinstance(obj, (str, int, float, bool)):
@@ -455,9 +416,7 @@ class AstParser:
455
416
  cleaned = clean_object(schema)
456
417
  return cleaned if cleaned else {"type": "object"}
457
418
 
458
- def _extract_param_schema_from_hint(
459
- self, type_hint, param_name: str
460
- ) -> dict[str, Any] | None:
419
+ def _extract_param_schema_from_hint(self, type_hint: Any, param_name: str) -> dict[str, Any] | None:
461
420
  """Extract parameter schema from type hint (including Annotated types)."""
462
421
  from typing import get_args, get_origin
463
422
 
@@ -467,11 +426,7 @@ class AstParser:
467
426
  args = get_args(type_hint)
468
427
 
469
428
  # Check for Annotated[Type, Field(...)]
470
- if (
471
- hasattr(origin, "__name__")
472
- and origin.__name__ == "Annotated"
473
- and len(args) >= 2
474
- ):
429
+ if hasattr(origin, "__name__") and origin.__name__ == "Annotated" and len(args) >= 2:
475
430
  base_type = args[0]
476
431
  metadata = args[1:]
477
432
 
@@ -493,7 +448,7 @@ class AstParser:
493
448
  # For non-Annotated types, just convert the type
494
449
  return self._type_to_schema(type_hint)
495
450
 
496
- def _type_to_schema(self, type_hint) -> dict[str, Any]:
451
+ def _type_to_schema(self, type_hint: object) -> dict[str, Any]:
497
452
  """Convert a Python type to JSON schema."""
498
453
  from typing import get_args, get_origin
499
454
  import types
@@ -577,9 +532,7 @@ class AstParser:
577
532
  # Check if it inherits from BaseModel
578
533
  for base in input_class.bases:
579
534
  if isinstance(base, ast.Name) and base.id == "BaseModel":
580
- component.input_schema = self._extract_pydantic_schema_from_ast(
581
- input_class
582
- )
535
+ component.input_schema = self._extract_pydantic_schema_from_ast(input_class)
583
536
  break
584
537
 
585
538
  # Process Output class if found
@@ -587,9 +540,7 @@ class AstParser:
587
540
  # Check if it inherits from BaseModel
588
541
  for base in output_class.bases:
589
542
  if isinstance(base, ast.Name) and base.id == "BaseModel":
590
- component.output_schema = self._extract_pydantic_schema_from_ast(
591
- output_class
592
- )
543
+ component.output_schema = self._extract_pydantic_schema_from_ast(output_class)
593
544
  break
594
545
 
595
546
  # Store annotations if found
@@ -602,24 +553,25 @@ class AstParser:
602
553
  for node in tree.body:
603
554
  if isinstance(node, ast.Assign):
604
555
  for target in node.targets:
605
- if isinstance(target, ast.Name) and target.id == "resource_uri":
606
- if isinstance(node.value, ast.Constant):
607
- uri_template = node.value.value
608
- component.uri_template = uri_template
556
+ if (
557
+ isinstance(target, ast.Name)
558
+ and target.id == "resource_uri"
559
+ and isinstance(node.value, ast.Constant)
560
+ ):
561
+ uri_template = node.value.value
562
+ component.uri_template = uri_template
609
563
 
610
- # Extract URI parameters (parts in {})
611
- uri_params = re.findall(r"{([^}]+)}", uri_template)
612
- if uri_params:
613
- component.parameters = uri_params
614
- break
564
+ # Extract URI parameters (parts in {})
565
+ uri_params = re.findall(r"{([^}]+)}", uri_template)
566
+ if uri_params:
567
+ component.parameters = uri_params
568
+ break
615
569
 
616
570
  def _process_prompt(self, component: ParsedComponent, tree: ast.Module) -> None:
617
571
  """Process a prompt component (no special processing needed)."""
618
572
  pass
619
573
 
620
- def _derive_component_name(
621
- self, file_path: Path, component_type: ComponentType
622
- ) -> str:
574
+ def _derive_component_name(self, file_path: Path, component_type: ComponentType) -> str:
623
575
  """Derive a component name from its file path according to the spec.
624
576
 
625
577
  Following the spec: <filename> + ("_" + "_".join(PathRev) if PathRev else "")
@@ -652,9 +604,7 @@ class AstParser:
652
604
  else:
653
605
  return filename
654
606
 
655
- def _extract_pydantic_schema_from_ast(
656
- self, class_node: ast.ClassDef
657
- ) -> dict[str, Any]:
607
+ def _extract_pydantic_schema_from_ast(self, class_node: ast.ClassDef) -> dict[str, Any]:
658
608
  """Extract a JSON schema from an AST class definition.
659
609
 
660
610
  This is a simplified version that extracts basic field information.
@@ -711,10 +661,7 @@ class AstParser:
711
661
  ):
712
662
  # Field object - extract its parameters
713
663
  for keyword in node.value.keywords:
714
- if (
715
- keyword.arg == "default"
716
- or keyword.arg == "default_factory"
717
- ):
664
+ if keyword.arg == "default" or keyword.arg == "default_factory":
718
665
  if isinstance(keyword.value, ast.Constant):
719
666
  prop["default"] = keyword.value.value
720
667
  elif keyword.arg == "description":
@@ -724,14 +671,11 @@ class AstParser:
724
671
  if isinstance(keyword.value, ast.Constant):
725
672
  prop["title"] = keyword.value.value
726
673
 
727
- # Check for position default argument (Field(..., "description"))
674
+ # Check for position default argument
675
+ # (Field(..., "description"))
728
676
  if node.value.args:
729
677
  for i, arg in enumerate(node.value.args):
730
- if (
731
- i == 0
732
- and isinstance(arg, ast.Constant)
733
- and arg.value != Ellipsis
734
- ):
678
+ if i == 0 and isinstance(arg, ast.Constant) and arg.value != Ellipsis:
735
679
  prop["default"] = arg.value
736
680
  elif i == 1 and isinstance(arg, ast.Constant):
737
681
  prop["description"] = arg.value
@@ -749,20 +693,16 @@ class AstParser:
749
693
  and isinstance(node.value.func, ast.Name)
750
694
  and node.value.func.id == "Field"
751
695
  ):
752
- # Field has default if it doesn't use ... or if it has a default keyword
696
+ # Field has default if it doesn't use ... or if it has a
697
+ # default keyword
753
698
  has_ellipsis = False
754
699
  has_default = False
755
700
 
756
- if node.value.args and isinstance(
757
- node.value.args[0], ast.Constant
758
- ):
701
+ if node.value.args and isinstance(node.value.args[0], ast.Constant):
759
702
  has_ellipsis = node.value.args[0].value is Ellipsis
760
703
 
761
704
  for keyword in node.value.keywords:
762
- if (
763
- keyword.arg == "default"
764
- or keyword.arg == "default_factory"
765
- ):
705
+ if keyword.arg == "default" or keyword.arg == "default_factory":
766
706
  has_default = True
767
707
 
768
708
  is_required = has_ellipsis and not has_default
@@ -863,16 +803,12 @@ class AstParser:
863
803
  result[key_str] = value.s
864
804
  elif isinstance(value, ast.Num): # For older Python versions
865
805
  result[key_str] = value.n
866
- elif isinstance(
867
- value, ast.NameConstant
868
- ): # For older Python versions (True/False/None)
806
+ elif isinstance(value, ast.NameConstant): # For older Python versions (True/False/None)
869
807
  result[key_str] = value.value
870
808
  elif isinstance(value, ast.Name):
871
809
  # Handle True/False/None as names
872
810
  if value.id in ("True", "False", "None"):
873
- result[key_str] = {"True": True, "False": False, "None": None}[
874
- value.id
875
- ]
811
+ result[key_str] = {"True": True, "False": False, "None": None}[value.id]
876
812
  # We could add more complex value handling here if needed
877
813
 
878
814
  return result
@@ -937,9 +873,7 @@ class AstParser:
937
873
  type_str = ast.unparse(subscript.slice)
938
874
  return {"type": self._type_hint_to_json_type(type_str)}
939
875
 
940
- def _is_parameter_required(
941
- self, position: int, defaults: list, total_args: int
942
- ) -> bool:
876
+ def _is_parameter_required(self, position: int, defaults: list, total_args: int) -> bool:
943
877
  """Check if a function parameter is required (has no default value)."""
944
878
  if position >= total_args or position < 0:
945
879
  return True # Default to required if position is out of range
@@ -953,12 +887,11 @@ class AstParser:
953
887
  args_with_defaults = len(defaults)
954
888
  first_default_position = total_args - args_with_defaults
955
889
 
956
- # If this parameter's position is before the first default position, it's required
890
+ # If this parameter's position is before the first default position,
891
+ # it's required
957
892
  return position < first_default_position
958
893
 
959
- def _extract_return_type_schema(
960
- self, return_annotation: ast.AST, tree: ast.Module
961
- ) -> dict[str, Any] | None:
894
+ def _extract_return_type_schema(self, return_annotation: ast.AST, tree: ast.Module) -> dict[str, Any] | None:
962
895
  """Extract schema from function return type annotation."""
963
896
  if isinstance(return_annotation, ast.Name):
964
897
  # Simple type like str, int, or a class name
@@ -977,9 +910,7 @@ class AstParser:
977
910
  type_str = ast.unparse(return_annotation)
978
911
  return {"type": self._type_hint_to_json_type(type_str)}
979
912
 
980
- def _find_class_schema(
981
- self, class_name: str, tree: ast.Module
982
- ) -> dict[str, Any] | None:
913
+ def _find_class_schema(self, class_name: str, tree: ast.Module) -> dict[str, Any] | None:
983
914
  """Find a class definition in the module and extract its schema."""
984
915
  for node in tree.body:
985
916
  if isinstance(node, ast.ClassDef) and node.name == class_name:
@@ -1010,18 +941,14 @@ def parse_project(project_path: Path) -> dict[ComponentType, list[ParsedComponen
1010
941
  dir_path = project_path / dir_name
1011
942
  if dir_path.exists() and dir_path.is_dir():
1012
943
  dir_components = parser.parse_directory(dir_path)
1013
- components[comp_type].extend(
1014
- [c for c in dir_components if c.type == comp_type]
1015
- )
944
+ components[comp_type].extend([c for c in dir_components if c.type == comp_type])
1016
945
 
1017
946
  # Check for ID collisions
1018
947
  all_ids = []
1019
948
  for comp_type, comps in components.items():
1020
949
  for comp in comps:
1021
950
  if comp.name in all_ids:
1022
- raise ValueError(
1023
- f"ID collision detected: {comp.name} is used by multiple components"
1024
- )
951
+ raise ValueError(f"ID collision detected: {comp.name} is used by multiple components")
1025
952
  all_ids.append(comp.name)
1026
953
 
1027
954
  return components
@@ -1047,9 +974,7 @@ def parse_common_files(project_path: Path) -> dict[str, Path]:
1047
974
  # Find all common.py files (recursively)
1048
975
  for common_file in base_dir.glob("**/common.py"):
1049
976
  # Skip files in __pycache__ or other hidden directories
1050
- if "__pycache__" in common_file.parts or any(
1051
- part.startswith(".") for part in common_file.parts
1052
- ):
977
+ if "__pycache__" in common_file.parts or any(part.startswith(".") for part in common_file.parts):
1053
978
  continue
1054
979
 
1055
980
  # Get the parent directory as the module path
@@ -1057,3 +982,71 @@ def parse_common_files(project_path: Path) -> dict[str, Path]:
1057
982
  common_files[module_path] = common_file
1058
983
 
1059
984
  return common_files
985
+
986
+
987
+ def _is_golf_component_file(file_path: Path) -> bool:
988
+ """Check if a Python file is a Golf component (has export or resource_uri).
989
+
990
+ Args:
991
+ file_path: Path to the Python file to check
992
+
993
+ Returns:
994
+ True if the file appears to be a Golf component, False otherwise
995
+ """
996
+ try:
997
+ with open(file_path, encoding="utf-8") as f:
998
+ content = f.read()
999
+
1000
+ # Parse the file to check for Golf component patterns
1001
+ tree = ast.parse(content)
1002
+
1003
+ # Look for 'export' or 'resource_uri' variable assignments
1004
+ for node in ast.walk(tree):
1005
+ if isinstance(node, ast.Assign):
1006
+ for target in node.targets:
1007
+ if isinstance(target, ast.Name):
1008
+ if target.id in ("export", "resource_uri"):
1009
+ return True
1010
+
1011
+ return False
1012
+
1013
+ except (SyntaxError, OSError, UnicodeDecodeError):
1014
+ # If we can't parse the file, assume it's not a component
1015
+ return False
1016
+
1017
+
1018
+ def parse_shared_files(project_path: Path) -> dict[str, Path]:
1019
+ """Find all shared Python files in the project (non-component .py files).
1020
+
1021
+ Args:
1022
+ project_path: Path to the project root
1023
+
1024
+ Returns:
1025
+ Dictionary mapping module paths to shared file paths
1026
+ """
1027
+ shared_files = {}
1028
+
1029
+ # Search for all .py files in tools, resources, and prompts directories
1030
+ for dir_name in ["tools", "resources", "prompts"]:
1031
+ base_dir = project_path / dir_name
1032
+ if not base_dir.exists() or not base_dir.is_dir():
1033
+ continue
1034
+
1035
+ # Find all .py files (recursively)
1036
+ for py_file in base_dir.glob("**/*.py"):
1037
+ # Skip files in __pycache__ or other hidden directories
1038
+ if "__pycache__" in py_file.parts or any(part.startswith(".") for part in py_file.parts):
1039
+ continue
1040
+
1041
+ # Skip files that are Golf components (have export or resource_uri)
1042
+ if _is_golf_component_file(py_file):
1043
+ continue
1044
+
1045
+ # Calculate the module path for this shared file
1046
+ # For example: tools/weather/helpers.py -> tools/weather/helpers
1047
+ relative_path = py_file.relative_to(project_path)
1048
+ module_path = str(relative_path.with_suffix("")) # Remove .py extension
1049
+
1050
+ shared_files[module_path] = py_file
1051
+
1052
+ return shared_files
golf/core/platform.py CHANGED
@@ -12,6 +12,14 @@ from golf import __version__
12
12
  from golf.core.config import Settings
13
13
  from golf.core.parser import ComponentType, ParsedComponent
14
14
 
15
+ # Import endpoints with fallback for dev mode
16
+ try:
17
+ # In built wheels, this exists (generated from _endpoints.py.in)
18
+ from golf import _endpoints # type: ignore
19
+ except ImportError:
20
+ # In editable/dev installs, fall back to env-based values
21
+ from golf import _endpoints_fallback as _endpoints # type: ignore
22
+
15
23
  console = Console()
16
24
 
17
25
 
@@ -65,7 +73,7 @@ async def register_project_with_platform(
65
73
  try:
66
74
  async with httpx.AsyncClient(timeout=10.0) as client:
67
75
  response = await client.post(
68
- "http://localhost:8000/api/resources",
76
+ _endpoints.PLATFORM_API_URL,
69
77
  json=metadata,
70
78
  headers={
71
79
  "X-Golf-Key": api_key,
@@ -83,17 +91,11 @@ async def register_project_with_platform(
83
91
  return False
84
92
  except httpx.HTTPStatusError as e:
85
93
  if e.response.status_code == 401:
86
- console.print(
87
- "[yellow]Warning: Platform registration failed - invalid API key[/yellow]"
88
- )
94
+ console.print("[yellow]Warning: Platform registration failed - invalid API key[/yellow]")
89
95
  elif e.response.status_code == 403:
90
- console.print(
91
- "[yellow]Warning: Platform registration failed - access denied[/yellow]"
92
- )
96
+ console.print("[yellow]Warning: Platform registration failed - access denied[/yellow]")
93
97
  else:
94
- console.print(
95
- f"[yellow]Warning: Platform registration failed - HTTP {e.response.status_code}[/yellow]"
96
- )
98
+ console.print(f"[yellow]Warning: Platform registration failed - HTTP {e.response.status_code}[/yellow]")
97
99
  return False
98
100
  except Exception as e:
99
101
  console.print(f"[yellow]Warning: Platform registration failed: {e}[/yellow]")
golf/core/telemetry.py CHANGED
@@ -82,7 +82,7 @@ def is_telemetry_enabled() -> bool:
82
82
  2. GOLF_TEST_MODE environment variable (always disabled in test mode)
83
83
  3. GOLF_TELEMETRY environment variable
84
84
  4. Persistent preference file
85
- 5. Default to True (opt-out model)
85
+ 5. Default to False (opt-in model)
86
86
  """
87
87
  global _telemetry_enabled
88
88
 
@@ -144,14 +144,11 @@ def get_anonymous_id() -> str:
144
144
  if id_file.exists():
145
145
  try:
146
146
  _anonymous_id = id_file.read_text().strip()
147
- # Check if ID is in the old format (no hyphen between hash and random component)
147
+ # Check if ID is in the old format (no hyphen between hash and
148
+ # random component)
148
149
  # Old format: golf-[8 chars hash][8 chars random]
149
150
  # New format: golf-[8 chars hash]-[8 chars random]
150
- if (
151
- _anonymous_id
152
- and _anonymous_id.startswith("golf-")
153
- and len(_anonymous_id) == 21
154
- ):
151
+ if _anonymous_id and _anonymous_id.startswith("golf-") and len(_anonymous_id) == 21:
155
152
  # This is likely the old format, regenerate
156
153
  _anonymous_id = None
157
154
  elif _anonymous_id:
@@ -163,9 +160,7 @@ def get_anonymous_id() -> str:
163
160
  # Use only non-identifying system information
164
161
 
165
162
  # Combine non-identifying factors for uniqueness
166
- machine_data = (
167
- f"{platform.machine()}-{platform.system()}-{platform.python_version()}"
168
- )
163
+ machine_data = f"{platform.machine()}-{platform.system()}-{platform.python_version()}"
169
164
  machine_hash = hashlib.sha256(machine_data.encode()).hexdigest()[:8]
170
165
 
171
166
  # Add a random component to ensure uniqueness
@@ -264,7 +259,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
264
259
  "$set": {
265
260
  "golf_version": __version__,
266
261
  "os": platform.system(),
267
- "python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
262
+ "python_version": (f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}"),
268
263
  }
269
264
  }
270
265
 
@@ -284,7 +279,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
284
279
  # Only include minimal, non-identifying properties
285
280
  safe_properties = {
286
281
  "golf_version": __version__,
287
- "python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
282
+ "python_version": (f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}"),
288
283
  "os": platform.system(),
289
284
  # Explicitly disable IP tracking and GeoIP enrichment
290
285
  "$ip": "0", # Override IP to prevent collection
@@ -331,7 +326,8 @@ def track_command(
331
326
  Args:
332
327
  command: The command being executed (e.g., "init", "build", "run")
333
328
  success: Whether the command was successful
334
- error_type: Type of error if command failed (e.g., "ValueError", "FileNotFoundError")
329
+ error_type: Type of error if command failed (e.g., "ValueError",
330
+ "FileNotFoundError")
335
331
  error_message: Sanitized error message (no sensitive data)
336
332
  """
337
333
  properties = {"success": success}
@@ -409,9 +405,7 @@ def track_detailed_error(
409
405
 
410
406
  # Add system context for debugging
411
407
  try:
412
- properties["python_executable"] = _sanitize_error_message(
413
- platform.python_implementation()
414
- )
408
+ properties["python_executable"] = _sanitize_error_message(platform.python_implementation())
415
409
  properties["platform_detail"] = platform.platform()[:50] # Limit length
416
410
  except Exception:
417
411
  pass
@@ -459,9 +453,7 @@ def _sanitize_error_message(message: str) -> str:
459
453
  message = re.sub(r"Bearer\s+[a-zA-Z0-9_.-]+", "Bearer [REDACTED]", message)
460
454
 
461
455
  # Remove email addresses
462
- message = re.sub(
463
- r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", message
464
- )
456
+ message = re.sub(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", message)
465
457
 
466
458
  # Remove IP addresses
467
459
  message = re.sub(r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b", "[IP]", message)