maxc-cli 0.4.3__tar.gz → 0.4.6__tar.gz

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.
Files changed (95) hide show
  1. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/PKG-INFO +1 -1
  2. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/setup.py +1 -1
  3. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/_samples.py +2 -1
  5. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/agent_platforms.py +10 -1
  6. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/app.py +103 -30
  7. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/catalog.py +4 -1
  8. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/data.py +15 -6
  9. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/cli.py +2 -2
  10. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/SKILL.md +3 -1
  11. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/bootstrap-auth.md +36 -16
  12. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/bootstrap-flow.md +4 -2
  13. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/PKG-INFO +1 -1
  14. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_agent_hints_and_cli.py +7 -1
  15. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_agent_platforms.py +5 -2
  16. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_agent_skill_commands_context.py +46 -3
  17. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_backend_data_serialization.py +1 -1
  18. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_cli_mock.py +296 -0
  19. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_integration_real.py +1 -1
  20. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/MANIFEST.in +0 -0
  21. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/README.md +0 -0
  22. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/pyproject.toml +0 -0
  23. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/scripts/pyinstaller_entry.py +0 -0
  24. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/scripts/regression_test.py +0 -0
  25. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/setup.cfg +0 -0
  26. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/__main__.py +0 -0
  27. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/audit.py +0 -0
  28. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/auth_providers.py +0 -0
  29. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/__init__.py +0 -0
  30. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/auth.py +0 -0
  31. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/job.py +0 -0
  32. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/meta.py +0 -0
  33. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/odps.py +0 -0
  34. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/query.py +0 -0
  35. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/cache.py +0 -0
  36. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/catalog_bootstrap.py +0 -0
  37. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/config.py +0 -0
  38. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/exceptions.py +0 -0
  39. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/help_format.py +0 -0
  40. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/helpers.py +0 -0
  41. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/masking.py +0 -0
  42. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/models.py +0 -0
  43. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/output.py +0 -0
  44. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/setting_parser.py +0 -0
  45. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  46. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/command-patterns.md +0 -0
  47. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  48. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  49. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  50. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  51. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/red-lines.md +0 -0
  52. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/setup-install.md +0 -0
  53. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  54. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  55. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  56. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/store.py +0 -0
  57. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/utils.py +0 -0
  58. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
  59. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  60. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  61. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/requires.txt +0 -0
  62. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/top_level.txt +0 -0
  63. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_agent_skill_commands.py +0 -0
  64. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_backend_data.py +0 -0
  65. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_backend_meta.py +0 -0
  66. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_build_release_script.py +0 -0
  67. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_cache.py +0 -0
  68. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_catalog.py +0 -0
  69. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_catalog_bootstrap.py +0 -0
  70. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_cli_arg_validation.py +0 -0
  71. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  72. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_compat.py +0 -0
  73. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_e2e_smoke.py +0 -0
  74. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_envelope_shape.py +0 -0
  75. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_error_self_correction.py +0 -0
  76. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_error_translation.py +0 -0
  77. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_exit_codes.py +0 -0
  78. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_external_auth.py +0 -0
  79. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_flag_hoist.py +0 -0
  80. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_help_format.py +0 -0
  81. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_help_version_e2e.py +0 -0
  82. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_helpers.py +0 -0
  83. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_helpers_csv.py +0 -0
  84. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_integration.py +0 -0
  85. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_job_improvements.py +0 -0
  86. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_masking.py +0 -0
  87. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_meta_schema_and_partition_cols.py +0 -0
  88. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_phase1_improvements.py +0 -0
  89. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_pyinstaller_bundle.py +0 -0
  90. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_query_auto_promote.py +0 -0
  91. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_query_result_csv_fallback.py +0 -0
  92. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_setting_parser.py +0 -0
  93. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_skill_cli_consistency.py +0 -0
  94. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_skill_eval.py +0 -0
  95. {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_skill_renderer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.4.3
3
+ Version: 0.4.6
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="maxc-cli",
12
- version="0.4.3",
12
+ version="0.4.6",
13
13
  description="Agent-native MaxCompute CLI for external coding agents",
14
14
  long_description=README.read_text(encoding="utf-8"),
15
15
  long_description_content_type="text/markdown",
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.4.3"
5
+ __version__ = "0.4.6"
@@ -152,7 +152,8 @@ SAMPLES: dict[str, str] = {
152
152
  "agent.skill": "maxc agent skill\nmaxc agent skill --json",
153
153
  "agent.skill.install": (
154
154
  "maxc agent skill install claude-code\n"
155
- "maxc agent skill install cursor --invocation aliyun-maxc"
155
+ "maxc agent skill install cursor --invocation aliyun-maxc\n"
156
+ "maxc agent skill install others --dir /path/to/skills"
156
157
  ),
157
158
  "agent.skill.update": (
158
159
  "maxc agent skill update cursor\n"
@@ -122,7 +122,8 @@ def _build_registry() -> tuple[Platform, ...]:
122
122
  ),
123
123
  Platform(name="cursor", install_root=_simple_root(".cursor"),
124
124
  next_step_hint="Restart Cursor to activate"),
125
- Platform(name="windsurf", install_root=_simple_root(".windsurf"),
125
+ Platform(name="windsurf",
126
+ install_root=Path.home() / ".codeium" / "windsurf" / "skills" / "maxc-cli",
126
127
  next_step_hint="Restart Windsurf to activate"),
127
128
  Platform(name="codex", install_root=_codex_root(),
128
129
  next_step_hint="Restart Codex to activate"),
@@ -132,6 +133,14 @@ def _build_registry() -> tuple[Platform, ...]:
132
133
  next_step_hint="Restart Qoder to activate"),
133
134
  Platform(name="qoderwork", install_root=_simple_root(".qoderwork"),
134
135
  next_step_hint="Restart QoderWork to activate"),
136
+ Platform(name="openclaw",
137
+ install_root=Path.home() / ".openclaw" / "workspace" / "skills" / "maxc-cli",
138
+ next_step_hint="Restart OpenClaw to activate"),
139
+ Platform(name="hermes", install_root=_simple_root(".hermes"),
140
+ next_step_hint="Restart Hermes to activate"),
141
+ Platform(name="others", install_root=Path("__dir_required__"),
142
+ requires_dir=False,
143
+ next_step_hint="Point your agent at the installed skill directory"),
135
144
  )
136
145
 
137
146
 
@@ -126,6 +126,12 @@ class _PickerInputs:
126
126
  reselect: 'bool' = False
127
127
 
128
128
 
129
+ class ProjectPickerPending(Exception):
130
+ """Raised when non-TTY auth login can list projects but needs the caller to pick one."""
131
+ def __init__(self, projects: list):
132
+ self.projects = projects
133
+
134
+
129
135
  class MaxCApp:
130
136
  def __init__(
131
137
  self,
@@ -1262,7 +1268,7 @@ class MaxCApp:
1262
1268
 
1263
1269
  if use_catalog and self.backend is not None:
1264
1270
  catalog_matches = self.backend.catalog_search_tables(
1265
- keyword, schema=effective_schema,
1271
+ keyword, schema=effective_schema, project=project,
1266
1272
  )
1267
1273
  if catalog_matches is not None:
1268
1274
  matches = catalog_matches
@@ -1735,7 +1741,7 @@ class MaxCApp:
1735
1741
  }
1736
1742
  )
1737
1743
 
1738
- all_tables, _ = self.backend.list_tables(schema=schema_name)
1744
+ all_tables, _ = self.backend.list_tables(schema=schema_name, project=target_project)
1739
1745
  tables = all_tables
1740
1746
 
1741
1747
  if progress_callback is not None:
@@ -1830,13 +1836,16 @@ class MaxCApp:
1830
1836
  }
1831
1837
  )
1832
1838
 
1839
+ write_schema = schema_name or "default"
1840
+
1833
1841
  def fetch_and_cache(
1834
1842
  table_name: 'str',
1835
- table_schema: 'str' = "default",
1836
1843
  ) -> 'tuple[str, str | None]':
1837
1844
  try:
1838
- full_table = self.backend.describe_table(table_name)
1839
- existing = self.cache.get_cached_table(project, full_table.name, table_schema)
1845
+ full_table = self.backend.describe_table(
1846
+ table_name, project=project, schema=schema_name,
1847
+ )
1848
+ existing = self.cache.get_cached_table(project, full_table.name, write_schema)
1840
1849
  columns = [
1841
1850
  {"name": c.name, "type": c.type, "comment": c.comment}
1842
1851
  for c in full_table.columns
@@ -1847,7 +1856,7 @@ class MaxCApp:
1847
1856
  description=full_table.description,
1848
1857
  columns=columns,
1849
1858
  partitions=full_table.partitions,
1850
- schema_name=table_schema,
1859
+ schema_name=write_schema,
1851
1860
  )
1852
1861
  return ("updated" if existing else "created"), None
1853
1862
  except Exception as exc:
@@ -2615,29 +2624,63 @@ class MaxCApp:
2615
2624
 
2616
2625
  # Project / endpoint / region / tunnel — try the interactive Catalog
2617
2626
  # picker when the user did not pin a project explicitly.
2618
- (
2619
- picked_project,
2620
- derived_endpoint,
2621
- derived_region,
2622
- derived_tunnel,
2623
- picker_warnings,
2624
- ) = self._resolve_project_via_picker(
2625
- _PickerInputs(
2626
- provided_project=project,
2627
- provided_endpoint=endpoint,
2628
- provided_region=region_name,
2629
- provided_tunnel=tunnel_endpoint,
2630
- access_id=resolved_access_id,
2631
- secret=resolved_secret,
2632
- security_token=resolved_token,
2633
- catalog_endpoint=catalog_endpoint,
2634
- no_picker=no_picker,
2635
- from_env=from_env,
2636
- env_settings=env_settings,
2637
- existing_auth=existing_auth,
2638
- reselect=reselect,
2627
+ try:
2628
+ (
2629
+ picked_project,
2630
+ derived_endpoint,
2631
+ derived_region,
2632
+ derived_tunnel,
2633
+ picker_warnings,
2634
+ ) = self._resolve_project_via_picker(
2635
+ _PickerInputs(
2636
+ provided_project=project,
2637
+ provided_endpoint=endpoint,
2638
+ provided_region=region_name,
2639
+ provided_tunnel=tunnel_endpoint,
2640
+ access_id=resolved_access_id,
2641
+ secret=resolved_secret,
2642
+ security_token=resolved_token,
2643
+ catalog_endpoint=catalog_endpoint,
2644
+ no_picker=no_picker,
2645
+ from_env=from_env,
2646
+ env_settings=env_settings,
2647
+ existing_auth=existing_auth,
2648
+ reselect=reselect,
2649
+ )
2650
+ )
2651
+ except ProjectPickerPending as exc:
2652
+ projects_data = [
2653
+ {
2654
+ "project_id": p.project_id,
2655
+ "region": p.region,
2656
+ "endpoint": _catalog_bootstrap.region_to_endpoint(p.region),
2657
+ "owner": p.owner,
2658
+ "schema_enabled": p.schema_enabled,
2659
+ "description": p.description,
2660
+ }
2661
+ for p in exc.projects
2662
+ ]
2663
+ return Envelope(
2664
+ command="auth.login",
2665
+ status="pending",
2666
+ data={
2667
+ "reason": "project_selection_required",
2668
+ "projects": projects_data,
2669
+ "count": len(projects_data),
2670
+ },
2671
+ agent_hints=AgentHints(
2672
+ actions=[
2673
+ SuggestedAction(
2674
+ id="auth.login",
2675
+ title="Complete login with selected project",
2676
+ command="maxc auth login --project <project_id> --json",
2677
+ executable=False,
2678
+ placeholders={"project_id": "<project_id>"},
2679
+ ),
2680
+ ],
2681
+ warnings=[],
2682
+ ),
2639
2683
  )
2640
- )
2641
2684
 
2642
2685
  resolved_auth = AuthConfig(
2643
2686
  access_id=resolved_access_id,
@@ -3086,8 +3129,30 @@ class MaxCApp:
3086
3129
  [],
3087
3130
  )
3088
3131
 
3089
- # 2. Picker not viable → today's behavior (prompt or fail).
3132
+ # 2. Picker not viable (non-TTY or --no-picker).
3090
3133
  if inputs.no_picker or not sys.stdin.isatty():
3134
+ # Non-TTY + picker not disabled: list projects for structured output
3135
+ catalog_warning: str | None = None
3136
+ if not inputs.no_picker and not from_env:
3137
+ try:
3138
+ bootstrap_odps = _catalog_bootstrap.build_bootstrap_odps(
3139
+ access_id=inputs.access_id,
3140
+ secret_access_key=inputs.secret,
3141
+ security_token=inputs.security_token,
3142
+ endpoint=inputs.catalog_endpoint or provided_endpoint,
3143
+ )
3144
+ projects = _catalog_bootstrap.list_all_projects(bootstrap_odps)
3145
+ if projects:
3146
+ raise ProjectPickerPending(projects)
3147
+ except ProjectPickerPending:
3148
+ raise
3149
+ except Exception as exc:
3150
+ catalog_warning = (
3151
+ f"Could not list projects via Catalog API "
3152
+ f"({type(exc).__name__}: {exc}). "
3153
+ f"Falling back to saved/env config."
3154
+ )
3155
+
3091
3156
  prompted = self._resolve_login_value(
3092
3157
  provided=None,
3093
3158
  env_value=env_settings.get("project"),
@@ -3097,12 +3162,15 @@ class MaxCApp:
3097
3162
  secret=False,
3098
3163
  use_env=from_env,
3099
3164
  )
3165
+ warnings_out: list[str] = []
3166
+ if catalog_warning:
3167
+ warnings_out.append(catalog_warning)
3100
3168
  return (
3101
3169
  prompted,
3102
3170
  provided_endpoint,
3103
3171
  provided_region,
3104
3172
  provided_tunnel,
3105
- [],
3173
+ warnings_out,
3106
3174
  )
3107
3175
 
3108
3176
  # 3. Try the catalog picker.
@@ -3409,6 +3477,11 @@ class MaxCApp:
3409
3477
  platform = agent_platforms.resolve(platform_name)
3410
3478
  except KeyError as exc:
3411
3479
  raise ValidationError(str(exc))
3480
+ if dir_override is None and not platform.install_root.is_absolute():
3481
+ raise ValidationError(
3482
+ f"Platform {platform.name!r} has no default install path — "
3483
+ f"pass --dir <path> to specify where the skill should be installed."
3484
+ )
3412
3485
  target = agent_platforms.effective_target(platform, dir_override)
3413
3486
  return platform, target
3414
3487
 
@@ -192,6 +192,7 @@ class CatalogMixin:
192
192
  keyword: str,
193
193
  *,
194
194
  schema: 'str | None' = None,
195
+ project: 'str | None' = None,
195
196
  page_size: int = 50,
196
197
  ) -> 'list[dict[str, Any]] | None':
197
198
  """Search tables via Catalog API server-side full-text search.
@@ -199,6 +200,8 @@ class CatalogMixin:
199
200
  Args:
200
201
  keyword: Search term — matched against table name (substring).
201
202
  schema: Optional schema to scope the search.
203
+ project: Optional project to scope the search; default = config's
204
+ default_project.
202
205
  page_size: Results per page (max 100).
203
206
 
204
207
  Returns:
@@ -225,7 +228,7 @@ class CatalogMixin:
225
228
  # type=TABLE — required filter
226
229
  # project={proj} — scope to project
227
230
  parts = ["type=TABLE"]
228
- project = self.config.default_project
231
+ project = project or self.config.default_project
229
232
  if project:
230
233
  parts.append(f"project={project}")
231
234
  if keyword:
@@ -38,18 +38,27 @@ def _serialize_value(value: 'Any') -> 'Any':
38
38
  class DataMixin:
39
39
  """Mixin providing data sampling and profiling methods."""
40
40
 
41
- def _table_tunnel(self):
42
- """Return a TableTunnel for the current ODPS client.
41
+ def _table_tunnel(self, project: 'str | None' = None):
42
+ """Return a TableTunnel bound to ``project`` (or the client default).
43
43
 
44
44
  Real PyODPS `ODPS` instances do not expose a `.tunnel` attribute,
45
45
  so we construct `odps.tunnel.TableTunnel(odps=self.client)` lazily.
46
- Test doubles (FakeODPS) DO expose `.tunnel` directly honor that
47
- so existing FakeTunnel infrastructure keeps working.
46
+ The tunnel resolves tables against its own project, so a cross-project
47
+ download/upload (e.g. `--project other_proj`) MUST construct the tunnel
48
+ with that project — `create_*_session()` takes no project argument.
49
+
50
+ Test doubles (FakeODPS) DO expose `.tunnel` directly — honor that so
51
+ existing FakeTunnel infrastructure keeps working, and surface the
52
+ requested project on the instance for assertions.
48
53
  """
49
54
  existing = getattr(self.client, "tunnel", None)
50
55
  if existing is not None:
56
+ if project is not None:
57
+ existing.requested_project = project
51
58
  return existing
52
59
  from odps.tunnel import TableTunnel
60
+ if project:
61
+ return TableTunnel(odps=self.client, project=project)
53
62
  return TableTunnel(odps=self.client)
54
63
 
55
64
  def _resolve_partition_for_sample(
@@ -340,7 +349,7 @@ class DataMixin:
340
349
  create_session_kwargs["create_partition"] = True
341
350
  if schema:
342
351
  create_session_kwargs["schema"] = schema
343
- upload_session = self._table_tunnel().create_upload_session(
352
+ upload_session = self._table_tunnel(project=project or self.project).create_upload_session(
344
353
  definition.name, **create_session_kwargs,
345
354
  )
346
355
 
@@ -491,7 +500,7 @@ class DataMixin:
491
500
  download_kwargs: dict[str, Any] = {"partition_spec": partition}
492
501
  if schema:
493
502
  download_kwargs["schema"] = schema
494
- session = self._table_tunnel().create_download_session(
503
+ session = self._table_tunnel(project=project or self.project).create_download_session(
495
504
  definition.name, **download_kwargs,
496
505
  )
497
506
  total = session.count
@@ -1667,7 +1667,7 @@ def _handle_cache_build(app: MaxCApp, args: argparse.Namespace, stdout: TextIO)
1667
1667
  stdout.write("Fetching table list...\n")
1668
1668
  stdout.flush()
1669
1669
 
1670
- tables, _ = app.backend.list_tables()
1670
+ tables, _ = app.backend.list_tables(project=target_project, schema=schema_name)
1671
1671
  total = len(tables)
1672
1672
 
1673
1673
  if total == 0:
@@ -1698,7 +1698,7 @@ def _handle_cache_build(app: MaxCApp, args: argparse.Namespace, stdout: TextIO)
1698
1698
  def _do_fetch():
1699
1699
  # Use a simpler approach: only get table metadata without sample rows
1700
1700
  # to avoid potential hangs on table.head() or iterate_partitions()
1701
- table = app.backend._get_table(table_name)
1701
+ table = app.backend._get_table(table_name, project=target_project, schema=schema_name)
1702
1702
  # Force reload to get full schema info
1703
1703
  if hasattr(table, 'reload'):
1704
1704
  table.reload()
@@ -52,10 +52,11 @@ These are non-negotiable. See [references/red-lines.md](references/red-lines.md)
52
52
 
53
53
  ## Bootstrap Flow
54
54
 
55
- When `auth whoami --json` returns `configured=false` (no auth set up), follow [references/bootstrap-flow.md](references/bootstrap-flow.md) step by step. Three principles:
55
+ When `auth whoami --json` returns `configured=false` (no auth set up), follow [references/bootstrap-flow.md](references/bootstrap-flow.md) step by step. Key principles:
56
56
 
57
57
  1. **Never pick the auth method yourself** — always ask the user to choose between AK/SK and environment variables.
58
58
  2. **If `auth whoami` shows `auth_type=external`, the user is on an externally-managed credential provider — do NOT modify the auth config.** Treat the bootstrap as already done. Only `project`/`endpoint`/`schema` are safe to change (via `session set` or by re-running `auth login-external` with the same `--process-command`).
59
+ 3. **For AK/SK login, omit `--project` to discover available projects** — the CLI returns a `status="pending"` envelope listing all projects visible to the AK/SK (with endpoint/region pre-computed). Pick a project from the list (ask the user if ambiguous), then re-run with `--project <id>`. Skip this when the user already knows the target project.
59
60
 
60
61
  ## First Pass
61
62
 
@@ -209,6 +210,7 @@ For full command syntax and options, see [references/command-patterns.md](refere
209
210
  | Check who I am and where I'm pointed | `{{cli}} auth whoami --json` |
210
211
  | Set up auth from scratch | See Bootstrap Flow above |
211
212
  | Switch project/schema for this session | `{{cli}} session set --project P --schema S --json` |
213
+ | Re-select project (list available projects) | `{{cli}} auth login --reselect --json` → pick from `data.projects` → re-run with `--project` |
212
214
  | List tables | `{{cli}} meta list-tables --schema S --json` |
213
215
  | Get full schema of a table | `{{cli}} meta describe T --json` |
214
216
  | Find tables by keyword | `{{cli}} meta search KW --json` |
@@ -76,16 +76,13 @@ Then follow the matching section below.
76
76
 
77
77
  If `auth whoami` shows `auth_type=external` (or the saved config has `provider: external`), the user is on an externally-managed credential provider set up by another tool. **Do not run Step 2 or write a new auth block.** Treat the auth as already valid. Only `project`/`endpoint`/`schema` may be changed — via `{{cli}} session set ...` or by re-running `auth login-external` with the *same* `--process-command` and the new project/endpoint values.
78
78
 
79
- ### Always ask for project and endpoint
79
+ ### Project and endpoint selection
80
80
 
81
- **Regardless of auth method, always ask the user for `project` and `endpoint` explicitly.** Do not silently reuse values from an existing config file or environment variables.
81
+ **Path A (AK/SK):** omit `--project` the CLI queries the Catalog API and returns a `status="pending"` envelope listing all projects visible to the AK/SK, with endpoint/region pre-computed per project. Pick one from the list (ask the user if ambiguous), then re-run with `--project <id>` to complete login.
82
82
 
83
- If a current value is visible in the config or env, present it as a default optionbut the user must confirm or change it:
83
+ **Path B (env vars):** `--from-env` reads `MAXCOMPUTE_PROJECT` and `MAXCOMPUTE_ENDPOINT` from the shell environment. If either is missing, the command fails with a clear errorask the user to set them.
84
84
 
85
- > "Which MaxCompute project would you like to use? (current config shows: `<existing_project>`)"
86
- > "Which endpoint? (current config shows: `<existing_endpoint>`)"
87
-
88
- Never assume the existing project/endpoint is still correct.
85
+ **Explicit override:** if the user already knows the target project, pass `--project` and `--endpoint` directly to skip project discovery.
89
86
 
90
87
  ---
91
88
 
@@ -97,23 +94,33 @@ Use when the user has a long-lived AK/SK pair.
97
94
 
98
95
  - `access_key_id`
99
96
  - `access_key_secret`
100
- - `project` (MaxCompute project name)
101
- - `endpoint` (e.g. `http://service.cn-shanghai.maxcompute.aliyun.com/api`)
102
- - `region` (optional, e.g. `cn-shanghai`)
103
97
 
104
- ### Login command
98
+ The Catalog API provides `project`, `endpoint`, `region`, and `tunnel_endpoint` automatically. Only ask the user for these if they pass `--no-picker` or already know the target project.
99
+
100
+ ### Login command — Step 1: discover projects (recommended)
105
101
 
106
102
  ```bash
107
103
  {{cli}} auth login \
108
104
  --access-id "<access_key_id>" \
109
105
  --secret-access-key "<access_key_secret>" \
110
- --project "<project>" \
111
- --endpoint "<endpoint>" \
112
- --region "<region>" \
113
106
  --json
114
107
  ```
115
108
 
116
- Add `--no-validate` to save config without a remote identity check:
109
+ Returns `status="pending"` with `data.projects` a list of `{project_id, region, endpoint, owner, schema_enabled, description}`. Pick a project from the list and proceed to Step 2.
110
+
111
+ ### Login command — Step 2: complete login
112
+
113
+ ```bash
114
+ {{cli}} auth login \
115
+ --access-id "<access_key_id>" \
116
+ --secret-access-key "<access_key_secret>" \
117
+ --project "<project_id>" \
118
+ --json
119
+ ```
120
+
121
+ Returns `status="success"` with the configured identity. Endpoint/region are auto-derived from the project when omitted.
122
+
123
+ ### Login command (explicit — when user knows the target)
117
124
 
118
125
  ```bash
119
126
  {{cli}} auth login \
@@ -121,10 +128,23 @@ Add `--no-validate` to save config without a remote identity check:
121
128
  --secret-access-key "<access_key_secret>" \
122
129
  --project "<project>" \
123
130
  --endpoint "<endpoint>" \
124
- --no-validate \
131
+ --region "<region>" \
125
132
  --json
126
133
  ```
127
134
 
135
+ Add `--no-validate` to either form to save config without a remote identity check.
136
+
137
+ ### Picker flags
138
+
139
+ | Flag | Effect |
140
+ |------|--------|
141
+ | (omit `--project`) | Return pending envelope with project list from Catalog API (non-TTY) or interactive picker (TTY) |
142
+ | `--no-picker` | Disable project discovery; fall back to manual prompt for project/endpoint (CI escape hatch) |
143
+ | `--reselect` | Force project discovery even when a project is already saved in config (no effect with `--project` or `--no-picker`) |
144
+ | `--catalog-endpoint <url>` | Override the Catalog API URL (for non-China regions where auto-routing is unavailable) |
145
+
146
+ Fallback: if the Catalog API call fails (network, permissions, etc.), the CLI falls back to a manual prompt (TTY) or returns project=None (non-TTY).
147
+
128
148
  ### What it saves
129
149
 
130
150
  ```yaml
@@ -51,9 +51,11 @@ Then jump to the matching path in [bootstrap-auth.md](bootstrap-auth.md):
51
51
 
52
52
  If `auth whoami --json` shows `auth_type=external` (or `provider: external` in the saved config), the user is on an externally-managed credential provider. **Do not run Phase 2.** The auth is already set up — only `project`/`endpoint`/`schema` are safe to change via `session set` or by re-running the original `auth login-external` with updated `--project`/`--endpoint`. Treat bootstrap as complete and move to Phase 3.
53
53
 
54
- ### Always confirm project and endpoint
54
+ ### Project and endpoint
55
55
 
56
- Regardless of method, ask the user explicitly for `project` and `endpoint`. If a value is already in the config or env, present it as a default but require confirmation. See [bootstrap-auth.md](bootstrap-auth.md) §"Always ask for project and endpoint".
56
+ **Path A (AK/SK):** do NOT ask for project or endpoint upfront. Omit `--project` from `auth login` — the CLI returns a `pending` envelope listing available projects with pre-computed endpoints. Pick one (ask the user if ambiguous), then re-run with `--project <id>`. See [bootstrap-auth.md](bootstrap-auth.md) §"Project and endpoint selection".
57
+
58
+ **Path B (env vars):** `--from-env` reads project and endpoint from the shell. If either is missing, ask the user to export them before re-running.
57
59
 
58
60
  ### Dev vs production project check
59
61
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.4.3
3
+ Version: 0.4.6
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -194,7 +194,13 @@ class _StubMetaBackend:
194
194
  return tables[:limit], len(tables) > limit
195
195
  return tables, False
196
196
 
197
- def describe_table(self, table_name: 'str', *, project: 'str | None' = None) -> 'TableDefinition':
197
+ def describe_table(
198
+ self,
199
+ table_name: 'str',
200
+ *,
201
+ project: 'str | None' = None,
202
+ schema: 'str | None' = None,
203
+ ) -> 'TableDefinition':
198
204
  time.sleep(0.01)
199
205
  return _table(table_name)
200
206
 
@@ -11,11 +11,12 @@ import pytest
11
11
  from maxc_cli import agent_platforms as ap
12
12
 
13
13
 
14
- def test_registry_has_seven_canonical_platforms():
14
+ def test_registry_has_canonical_platforms():
15
15
  names = {p.name for p in ap.REGISTRY}
16
16
  assert names == {
17
17
  "claude-code", "cursor", "windsurf", "codex",
18
18
  "qwen", "qoder", "qoderwork",
19
+ "openclaw", "hermes", "others",
19
20
  }
20
21
 
21
22
 
@@ -41,10 +42,12 @@ def test_install_root_matches_legacy_paths():
41
42
  expected = {
42
43
  "claude-code": Path.home() / ".claude" / "skills" / "maxc-cli",
43
44
  "cursor": Path.home() / ".cursor" / "skills" / "maxc-cli",
44
- "windsurf": Path.home() / ".windsurf" / "skills" / "maxc-cli",
45
+ "windsurf": Path.home() / ".codeium" / "windsurf" / "skills" / "maxc-cli",
45
46
  "qwen": Path.home() / ".qwen" / "skills" / "maxc-cli",
46
47
  "qoder": Path.home() / ".qoder" / "skills" / "maxc-cli",
47
48
  "qoderwork": Path.home() / ".qoderwork" / "skills" / "maxc-cli",
49
+ "openclaw": Path.home() / ".openclaw" / "workspace" / "skills" / "maxc-cli",
50
+ "hermes": Path.home() / ".hermes" / "skills" / "maxc-cli",
48
51
  }
49
52
  for name, expected_path in expected.items():
50
53
  assert ap.resolve(name).install_root == expected_path, name
@@ -224,11 +224,13 @@ class TestAgentInstallSkill:
224
224
  for d in [
225
225
  Path.home() / ".claude" / "skills" / "maxc-cli",
226
226
  Path.home() / ".cursor" / "skills" / "maxc-cli",
227
- Path.home() / ".windsurf" / "skills" / "maxc-cli",
227
+ Path.home() / ".codeium" / "windsurf" / "skills" / "maxc-cli",
228
228
  Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))) / "skills" / "maxc-cli",
229
229
  Path.home() / ".qwen" / "skills" / "maxc-cli",
230
230
  Path.home() / ".qoder" / "skills" / "maxc-cli",
231
231
  Path.home() / ".qoderwork" / "skills" / "maxc-cli",
232
+ Path.home() / ".openclaw" / "workspace" / "skills" / "maxc-cli",
233
+ Path.home() / ".hermes" / "skills" / "maxc-cli",
232
234
  ]:
233
235
  if d.exists():
234
236
  shutil.rmtree(str(d))
@@ -236,11 +238,13 @@ class TestAgentInstallSkill:
236
238
  for d in [
237
239
  Path.home() / ".claude" / "skills" / "maxc-cli",
238
240
  Path.home() / ".cursor" / "skills" / "maxc-cli",
239
- Path.home() / ".windsurf" / "skills" / "maxc-cli",
241
+ Path.home() / ".codeium" / "windsurf" / "skills" / "maxc-cli",
240
242
  Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))) / "skills" / "maxc-cli",
241
243
  Path.home() / ".qwen" / "skills" / "maxc-cli",
242
244
  Path.home() / ".qoder" / "skills" / "maxc-cli",
243
245
  Path.home() / ".qoderwork" / "skills" / "maxc-cli",
246
+ Path.home() / ".openclaw" / "workspace" / "skills" / "maxc-cli",
247
+ Path.home() / ".hermes" / "skills" / "maxc-cli",
244
248
  ]:
245
249
  if d.exists():
246
250
  shutil.rmtree(str(d))
@@ -288,7 +292,7 @@ class TestAgentInstallSkill:
288
292
  assert data["platform"] == "windsurf"
289
293
  assert data["upgraded"] is True
290
294
  install_path = Path(data["install_path"])
291
- assert ".windsurf/skills" in str(install_path)
295
+ assert ".codeium/windsurf/skills" in str(install_path)
292
296
  assert (install_path / "SKILL.md").is_file()
293
297
 
294
298
  def test_install_skill_qwen(self, tmp_path):
@@ -324,6 +328,45 @@ class TestAgentInstallSkill:
324
328
  assert ".qoderwork/skills" in str(install_path)
325
329
  assert (install_path / "SKILL.md").is_file()
326
330
 
331
+ def test_install_skill_openclaw(self, tmp_path):
332
+ config = _make_config(tmp_path)
333
+ code, payload, _ = _run_cmd(config, ["agent", "skill", "install", "openclaw", "--json"])
334
+ assert code == 0
335
+ data = payload["data"]
336
+ assert data["platform"] == "openclaw"
337
+ assert data["upgraded"] is True
338
+ install_path = Path(data["install_path"])
339
+ assert ".openclaw/workspace/skills" in str(install_path)
340
+ assert (install_path / "SKILL.md").is_file()
341
+
342
+ def test_install_skill_hermes(self, tmp_path):
343
+ config = _make_config(tmp_path)
344
+ code, payload, _ = _run_cmd(config, ["agent", "skill", "install", "hermes", "--json"])
345
+ assert code == 0
346
+ data = payload["data"]
347
+ assert data["platform"] == "hermes"
348
+ assert data["upgraded"] is True
349
+ install_path = Path(data["install_path"])
350
+ assert ".hermes/skills" in str(install_path)
351
+ assert (install_path / "SKILL.md").is_file()
352
+
353
+ def test_install_skill_others_requires_dir(self, tmp_path):
354
+ config = _make_config(tmp_path)
355
+ code, payload, _ = _run_cmd(config, ["agent", "skill", "install", "others", "--json"])
356
+ assert code == 1
357
+ assert payload["status"] == "failure"
358
+ assert "--dir" in payload["error"]["message"]
359
+
360
+ def test_install_skill_others_with_dir(self, tmp_path):
361
+ config = _make_config(tmp_path)
362
+ target = tmp_path / "custom-agent-skill"
363
+ code, payload, _ = _run_cmd(config, ["agent", "skill", "install", "others", "--dir", str(target), "--json"])
364
+ assert code == 0
365
+ data = payload["data"]
366
+ assert data["platform"] == "others"
367
+ assert data["upgraded"] is True
368
+ assert (target / "SKILL.md").is_file()
369
+
327
370
  def test_install_skill_default_platform_is_claude_code(self, tmp_path):
328
371
  config = _make_config(tmp_path)
329
372
  code, payload, _ = _run_cmd(config, ["agent", "skill", "install", "--json"])
@@ -134,7 +134,7 @@ class _UploadHarness:
134
134
  def describe_table(self, *args, **kwargs) -> TableDefinition:
135
135
  return self._definition
136
136
 
137
- def _table_tunnel(self) -> _FakeTunnel:
137
+ def _table_tunnel(self, project: 'str | None' = None) -> _FakeTunnel:
138
138
  return self._tunnel
139
139
 
140
140
  def upload(self, *args, **kwargs):