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.
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/PKG-INFO +1 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/setup.py +1 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/_samples.py +2 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/agent_platforms.py +10 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/app.py +103 -30
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/catalog.py +4 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/data.py +15 -6
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/cli.py +2 -2
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/SKILL.md +3 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/bootstrap-auth.md +36 -16
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/bootstrap-flow.md +4 -2
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_agent_hints_and_cli.py +7 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_agent_platforms.py +5 -2
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_agent_skill_commands_context.py +46 -3
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_backend_data_serialization.py +1 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_cli_mock.py +296 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_integration_real.py +1 -1
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/MANIFEST.in +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/README.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/pyproject.toml +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/scripts/regression_test.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/setup.cfg +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/config.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/help_format.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/helpers.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_agent_skill_commands.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_cache.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_catalog.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_compat.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_external_auth.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_help_format.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_help_version_e2e.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_helpers.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_integration.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_masking.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_phase1_improvements.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_query_result_csv_fallback.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_setting_parser.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_skill_eval.py +0 -0
- {maxc_cli-0.4.3 → maxc_cli-0.4.6}/tests/test_skill_renderer.py +0 -0
|
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="maxc-cli",
|
|
12
|
-
version="0.4.
|
|
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",
|
|
@@ -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",
|
|
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(
|
|
1839
|
-
|
|
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=
|
|
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
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
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.
|
|
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
|
-
###
|
|
79
|
+
### Project and endpoint selection
|
|
80
80
|
|
|
81
|
-
**
|
|
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
|
-
|
|
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 error — ask the user to set them.
|
|
84
84
|
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
--
|
|
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
|
-
###
|
|
54
|
+
### Project and endpoint
|
|
55
55
|
|
|
56
|
-
|
|
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
|
|
|
@@ -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(
|
|
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
|
|
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"
|
|
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):
|