systemlink-cli 1.9.3__tar.gz → 1.11.0__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 (88) hide show
  1. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/PKG-INFO +1 -1
  2. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/pyproject.toml +38 -31
  3. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/_version.py +1 -1
  4. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/main.py +7 -0
  5. systemlink_cli-1.11.0/slcli/mcp_reachability.py +64 -0
  6. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/mcp_server.py +44 -5
  7. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/platform.py +104 -5
  8. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skill_click.py +51 -21
  9. systemlink_cli-1.11.0/slcli/skills/nipkg-file-package/SKILL.md +541 -0
  10. systemlink_cli-1.11.0/slcli/skills/slcli/SKILL.md +180 -0
  11. systemlink_cli-1.9.3/slcli/skills/slcli/SKILL.md → systemlink_cli-1.11.0/slcli/skills/slcli/references/commands.md +54 -125
  12. systemlink_cli-1.11.0/slcli/skills/slcli/references/datasheet-workflow.md +510 -0
  13. systemlink_cli-1.11.0/slcli/skills/slcli/references/troubleshooting.md +45 -0
  14. systemlink_cli-1.11.0/slcli/skills/systemlink-job-debugging/SKILL.md +325 -0
  15. systemlink_cli-1.11.0/slcli/skills/systemlink-python-test/SKILL.md +547 -0
  16. systemlink_cli-1.11.0/slcli/spec_click.py +2141 -0
  17. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/system_click.py +424 -65
  18. systemlink_cli-1.11.0/slcli/system_query_utils.py +161 -0
  19. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/utils.py +4 -1
  20. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/LICENSE +0 -0
  21. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/dff-editor/editor.js +0 -0
  22. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/dff-editor/index.html +0 -0
  23. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/__init__.py +0 -0
  24. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/__main__.py +0 -0
  25. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/asset_click.py +0 -0
  26. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/cli_formatters.py +0 -0
  27. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/cli_utils.py +0 -0
  28. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/comment_click.py +0 -0
  29. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/completion_click.py +0 -0
  30. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/config.py +0 -0
  31. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/config_click.py +0 -0
  32. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/dff_click.py +0 -0
  33. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/dff_decorators.py +0 -0
  34. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/example_click.py +0 -0
  35. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/example_loader.py +0 -0
  36. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/example_provisioner.py +0 -0
  37. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/README.md +0 -0
  38. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/_schema/schema-v1.0.json +0 -0
  39. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/demo-complete-workflow/README.md +0 -0
  40. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
  41. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/demo-test-plans/README.md +0 -0
  42. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/demo-test-plans/config.yaml +0 -0
  43. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
  44. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
  45. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
  46. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
  47. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
  48. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
  49. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
  50. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
  51. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
  52. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  53. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/feed_click.py +0 -0
  54. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/file_click.py +0 -0
  55. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/function_click.py +0 -0
  56. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/function_templates.py +0 -0
  57. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/mcp_click.py +0 -0
  58. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/notebook_click.py +0 -0
  59. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/policy_click.py +0 -0
  60. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/policy_utils.py +0 -0
  61. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/profiles.py +0 -0
  62. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/response_handlers.py +0 -0
  63. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/rich_output.py +0 -0
  64. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/routine_click.py +0 -0
  65. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
  66. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/slcli/references/filtering.md +0 -0
  67. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-notebook/SKILL.md +0 -0
  68. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-notebook/references/interfaces.md +0 -0
  69. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-notebook/references/notebook-patterns.md +0 -0
  70. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/SKILL.md +0 -0
  71. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
  72. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/references/layout-patterns.md +0 -0
  73. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
  74. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
  75. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/ssl_trust.py +0 -0
  76. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/table_utils.py +0 -0
  77. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/tag_click.py +0 -0
  78. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/templates_click.py +0 -0
  79. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/testmonitor_click.py +0 -0
  80. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/universal_handlers.py +0 -0
  81. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/user_click.py +0 -0
  82. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/web_editor.py +0 -0
  83. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/webapp_click.py +0 -0
  84. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workflow_preview.py +0 -0
  85. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workflows_click.py +0 -0
  86. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workitem_click.py +0 -0
  87. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workspace_click.py +0 -0
  88. {systemlink_cli-1.9.3 → systemlink_cli-1.11.0}/slcli/workspace_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: systemlink-cli
3
- Version: 1.9.3
3
+ Version: 1.11.0
4
4
  Summary: SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates.
5
5
  License-File: LICENSE
6
6
  Author: Fred Visser
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "systemlink-cli"
3
- version = "1.9.3"
3
+ version = "1.11.0"
4
4
  description = "SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates."
5
5
  authors = ["Fred Visser <fred.visser@emerson.com>"]
6
6
  packages = [{ include = "slcli" }]
@@ -16,6 +16,7 @@ slcli = "slcli.__main__:cli"
16
16
  slcli-mcp = "slcli.mcp_server:main"
17
17
  build-pyinstaller = "scripts.build_pyinstaller:main"
18
18
  update-version = "scripts.update_version:main"
19
+ release-from-changes = "scripts.towncrier_release:main"
19
20
  lint = "scripts.lint:main"
20
21
 
21
22
  [tool.poetry.group.mcp]
@@ -59,8 +60,8 @@ pyinstaller = "^6.14.2"
59
60
  types-requests = "*"
60
61
  types-tabulate = "*"
61
62
 
62
- # Semantic Release
63
- python-semantic-release = "^10.0.0"
63
+ # Changelog and release management
64
+ towncrier = "^25.8.0"
64
65
  pytest-xdist = "^3.8.0"
65
66
 
66
67
  # Excel file generation
@@ -114,34 +115,40 @@ check_untyped_defs = true
114
115
  warn_redundant_casts = true
115
116
  warn_unreachable = true
116
117
 
117
- [tool.semantic_release]
118
- version_toml = ["pyproject.toml:tool.poetry.version"]
119
- version_variables = ["slcli/_version.py:__version__"]
120
- build_command = "python scripts/update_version.py"
121
- major_on_zero = false
122
- branch = "main"
123
- upload_to_pypi = true
124
- upload_to_release = true
125
- hvcs = "github"
126
- commit_message = "chore(release): {version}"
127
- tag_format = "v{version}"
128
-
129
- [tool.semantic_release.commit_parser_options]
130
- allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"]
131
- minor_tags = ["feat"]
132
- patch_tags = ["fix", "perf"]
133
-
134
- [tool.semantic_release.changelog]
135
- template_dir = "templates"
136
- exclude_commit_patterns = []
137
-
138
- [tool.semantic_release.changelog.default_templates]
139
- changelog_file = "CHANGELOG.md"
140
-
141
- [tool.semantic_release.branches.main]
142
- match = "(main|master)"
143
- prerelease_token = "rc"
144
- prerelease = false
118
+ [tool.towncrier]
119
+ package = "slcli"
120
+ package_dir = "."
121
+ directory = "newsfragments"
122
+ filename = "CHANGELOG.md"
123
+ template = "towncrier:default.md"
124
+ start_string = "<!-- towncrier release notes start -->\n"
125
+ title_format = "## v{version} ({project_date})"
126
+ ignore = ["README.md"]
127
+
128
+ [[tool.towncrier.type]]
129
+ directory = "major"
130
+ name = "Breaking Changes"
131
+ showcontent = true
132
+
133
+ [[tool.towncrier.type]]
134
+ directory = "minor"
135
+ name = "Features"
136
+ showcontent = true
137
+
138
+ [[tool.towncrier.type]]
139
+ directory = "patch"
140
+ name = "Bug Fixes"
141
+ showcontent = true
142
+
143
+ [[tool.towncrier.type]]
144
+ directory = "doc"
145
+ name = "Documentation"
146
+ showcontent = true
147
+
148
+ [[tool.towncrier.type]]
149
+ directory = "misc"
150
+ name = "Other Changes"
151
+ showcontent = true
145
152
 
146
153
  [build-system]
147
154
  requires = ["poetry-core>=1.0.0"]
@@ -1,4 +1,4 @@
1
1
  """Version information for slcli."""
2
2
 
3
3
  # This file is auto-generated. Do not edit manually.
4
- __version__ = "1.9.3"
4
+ __version__ = "1.11.0"
@@ -30,6 +30,7 @@ from .rich_output import install_rich_output
30
30
  from .rich_output import render_table
31
31
  from .routine_click import register_routine_commands
32
32
  from .skill_click import register_skill_commands
33
+ from .spec_click import register_spec_commands
33
34
  from .ssl_trust import OS_TRUST_INJECTED, OS_TRUST_REASON
34
35
  from .system_click import register_system_commands
35
36
  from .tag_click import register_tag_commands
@@ -365,6 +366,11 @@ def info(format: str, skip_health: bool) -> None:
365
366
  workspace_display = truncate(workspace)
366
367
  info_rows.append(["Workspace", workspace_display])
367
368
 
369
+ system_query_endpoint = platform_info.get("system_query_endpoint")
370
+ if system_query_endpoint:
371
+ system_query_display = truncate(str(system_query_endpoint))
372
+ info_rows.append(["System Query", system_query_display])
373
+
368
374
  file_query_endpoint = platform_info.get("file_query_endpoint")
369
375
  if file_query_endpoint:
370
376
  if (
@@ -427,6 +433,7 @@ register_notebook_commands(cli)
427
433
  register_policy_commands(cli)
428
434
  register_routine_commands(cli)
429
435
  register_system_commands(cli)
436
+ register_spec_commands(cli)
430
437
  register_tag_commands(cli)
431
438
  register_testmonitor_commands(cli)
432
439
  register_webapp_commands(cli)
@@ -0,0 +1,64 @@
1
+ """Helpers for classifying local MCP client connectivity failures."""
2
+
3
+ from typing import Iterator, List
4
+
5
+
6
+ def iter_exceptions(exc: BaseException) -> Iterator[BaseException]:
7
+ """Yield an exception plus any nested causes, contexts, or exception-group members."""
8
+ stack: List[BaseException] = [exc]
9
+ seen: set[int] = set()
10
+
11
+ while stack:
12
+ current = stack.pop()
13
+ marker = id(current)
14
+ if marker in seen:
15
+ continue
16
+ seen.add(marker)
17
+ yield current
18
+
19
+ cause = getattr(current, "__cause__", None)
20
+ if isinstance(cause, BaseException):
21
+ stack.append(cause)
22
+
23
+ context = getattr(current, "__context__", None)
24
+ if isinstance(context, BaseException):
25
+ stack.append(context)
26
+
27
+ if isinstance(current, BaseExceptionGroup):
28
+ stack.extend(current.exceptions)
29
+
30
+
31
+ def is_reachability_failure(exc: BaseException) -> bool:
32
+ """Return True when an exception indicates the local MCP server is unreachable."""
33
+ for candidate in iter_exceptions(exc):
34
+ if isinstance(candidate, (OSError, TimeoutError)):
35
+ return True
36
+
37
+ message = str(candidate).lower()
38
+ if "/mcp" in message and any(
39
+ token in message
40
+ for token in (
41
+ "server error",
42
+ "not found",
43
+ "method not allowed",
44
+ "bad request",
45
+ )
46
+ ):
47
+ return True
48
+
49
+ if any(
50
+ token in message
51
+ for token in (
52
+ "connection refused",
53
+ "connect error",
54
+ "all connection attempts failed",
55
+ "timed out",
56
+ "timeout",
57
+ "name or service not known",
58
+ "nodename nor servname provided",
59
+ "server disconnected",
60
+ )
61
+ ):
62
+ return True
63
+
64
+ return False
@@ -271,7 +271,41 @@ def query_systems(
271
271
  take: int = 100,
272
272
  ) -> str:
273
273
  """Query systems by alias, connection state, workspace, or raw filter."""
274
- from .utils import get_base_url
274
+ import requests as requests_lib
275
+
276
+ from .system_query_utils import (
277
+ MATERIALIZED_SYSTEM_MCP_PROJECTION,
278
+ build_materialized_system_search_filter,
279
+ get_system_query_url,
280
+ get_system_search_url,
281
+ )
282
+ from .utils import make_api_request
283
+
284
+ materialized_filter = None
285
+ if filter is None:
286
+ materialized_filter = build_materialized_system_search_filter(
287
+ alias=alias,
288
+ state=state,
289
+ workspace_id=workspace,
290
+ )
291
+
292
+ if filter is None:
293
+ search_payload: Dict[str, Any] = {
294
+ "take": take,
295
+ "projection": MATERIALIZED_SYSTEM_MCP_PROJECTION,
296
+ }
297
+ if materialized_filter:
298
+ search_payload["filter"] = materialized_filter
299
+ try:
300
+ data = make_api_request(
301
+ "POST",
302
+ get_system_search_url(),
303
+ payload=search_payload,
304
+ handle_errors=False,
305
+ ).json()
306
+ return _dump(_normalize_systems(data))
307
+ except requests_lib.RequestException:
308
+ pass
275
309
 
276
310
  filter_parts: List[str] = []
277
311
  if alias:
@@ -283,11 +317,16 @@ def query_systems(
283
317
  if filter:
284
318
  filter_parts.append(filter)
285
319
 
286
- payload: Dict[str, Any] = {"take": take}
320
+ query_payload: Dict[str, Any] = {"take": take}
287
321
  if filter_parts:
288
- payload["filter"] = " and ".join(filter_parts)
289
-
290
- data = _post_json(f"{get_base_url()}/nisysmgmt/v1/query-systems", payload)
322
+ query_payload["filter"] = " and ".join(filter_parts)
323
+
324
+ data = make_api_request(
325
+ "POST",
326
+ get_system_query_url(),
327
+ payload=query_payload,
328
+ handle_errors=False,
329
+ ).json()
291
330
  return _dump(_normalize_systems(data))
292
331
 
293
332
 
@@ -59,6 +59,8 @@ FEATURE_DISPLAY_NAMES: Dict[str, str] = {
59
59
  FILE_SEARCH_PATH = "/nifile/v1/service-groups/Default/search-files"
60
60
  FILE_QUERY_PATH = "/nifile/v1/service-groups/Default/query-files"
61
61
  FILE_QUERY_LINQ_PATH = "/nifile/v1/service-groups/Default/query-files-linq"
62
+ SYSTEM_SEARCH_PATH = "/nisysmgmt/v1/materialized/search-systems"
63
+ SYSTEM_QUERY_PATH = "/nisysmgmt/v1/query-systems"
62
64
 
63
65
 
64
66
  def _get_keyring_config() -> Dict[str, Any]:
@@ -195,7 +197,7 @@ SERVICE_CHECKS: List[List[str]] = [
195
197
  ["Auth", "GET", "/niauth/v1/policies"],
196
198
  ["Test Monitor", "GET", "/nitestmonitor/v2/results?take=0"],
197
199
  ["Asset Management", "POST", "/niapm/v1/query-assets"],
198
- ["Systems", "POST", "/nisysmgmt/v1/query-systems"],
200
+ ["Systems", "POST", SYSTEM_QUERY_PATH],
199
201
  ["Tag", "GET", "/nitag/v2/tags?take=0"],
200
202
  ["File", "POST", FILE_SEARCH_PATH],
201
203
  ["Notebook", "POST", "/ninotebook/v1/notebook/query"],
@@ -319,6 +321,87 @@ def get_file_query_capability(api_url: str, api_key: str) -> Dict[str, Any]:
319
321
  }
320
322
 
321
323
 
324
+ def get_system_query_capability(api_url: str, api_key: str) -> Dict[str, Any]:
325
+ """Determine which systems query endpoint is available for this server."""
326
+ headers = {
327
+ "x-ni-api-key": api_key,
328
+ "Content-Type": "application/json",
329
+ "User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
330
+ }
331
+ ssl_verify = get_ssl_verify()
332
+
333
+ try:
334
+ search_resp = requests.post(
335
+ f"{api_url}{SYSTEM_SEARCH_PATH}",
336
+ headers=headers,
337
+ json={"take": 1, "projection": ["id"]},
338
+ verify=ssl_verify,
339
+ timeout=10,
340
+ )
341
+ if search_resp.status_code in (200, 400):
342
+ return {
343
+ "status": "ok",
344
+ "system_query_endpoint": "search-systems",
345
+ "materialized_search_available": True,
346
+ }
347
+ if search_resp.status_code in (401, 403):
348
+ return {
349
+ "status": "unauthorized",
350
+ "system_query_endpoint": "search-systems",
351
+ "materialized_search_available": True,
352
+ }
353
+ if search_resp.status_code not in (404, 501):
354
+ return {
355
+ "status": "error" if search_resp.status_code >= 500 else "not_found",
356
+ "system_query_endpoint": None,
357
+ "materialized_search_available": True,
358
+ }
359
+ except requests.RequestException:
360
+ return {
361
+ "status": "unreachable",
362
+ "system_query_endpoint": None,
363
+ "materialized_search_available": None,
364
+ }
365
+
366
+ try:
367
+ query_resp = requests.post(
368
+ f"{api_url}{SYSTEM_QUERY_PATH}",
369
+ headers=headers,
370
+ json={"take": 1, "projection": "new(id)"},
371
+ verify=ssl_verify,
372
+ timeout=10,
373
+ )
374
+ if query_resp.status_code in (200, 400):
375
+ return {
376
+ "status": "ok",
377
+ "system_query_endpoint": "query-systems",
378
+ "materialized_search_available": False,
379
+ }
380
+ if query_resp.status_code in (401, 403):
381
+ return {
382
+ "status": "unauthorized",
383
+ "system_query_endpoint": "query-systems",
384
+ "materialized_search_available": False,
385
+ }
386
+ if query_resp.status_code in (404, 501):
387
+ return {
388
+ "status": "not_found",
389
+ "system_query_endpoint": None,
390
+ "materialized_search_available": False,
391
+ }
392
+ return {
393
+ "status": "error",
394
+ "system_query_endpoint": None,
395
+ "materialized_search_available": False,
396
+ }
397
+ except requests.RequestException:
398
+ return {
399
+ "status": "unreachable",
400
+ "system_query_endpoint": None,
401
+ "materialized_search_available": False,
402
+ }
403
+
404
+
322
405
  def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
323
406
  """Probe key SystemLink services and report their status.
324
407
 
@@ -334,10 +417,12 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
334
417
  - auth_valid: bool | None - whether the API key is authorized (None if unreachable)
335
418
  - services: dict mapping service name to status string
336
419
  ("ok", "unauthorized", "not_found", "error", "unreachable")
337
- - file_query_endpoint: selected file query endpoint, if available
338
- - elasticsearch_available: bool | None - whether search-files is available
339
- - platform: detected platform string (PLATFORM_SLE, PLATFORM_SLS,
340
- PLATFORM_UNREACHABLE, PLATFORM_UNKNOWN)
420
+ - file_query_endpoint: selected file query endpoint, if available
421
+ - elasticsearch_available: bool | None - whether search-files is available
422
+ - system_query_endpoint: selected systems query endpoint, if available
423
+ - materialized_search_available: bool | None - whether search-systems is available
424
+ - platform: detected platform string (PLATFORM_SLE, PLATFORM_SLS,
425
+ PLATFORM_UNREACHABLE, PLATFORM_UNKNOWN)
341
426
  """
342
427
  headers = {
343
428
  "x-ni-api-key": api_key,
@@ -396,6 +481,8 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
396
481
  "services": services,
397
482
  "file_query_endpoint": None,
398
483
  "elasticsearch_available": None,
484
+ "system_query_endpoint": None,
485
+ "materialized_search_available": None,
399
486
  "platform": PLATFORM_UNREACHABLE,
400
487
  }
401
488
 
@@ -416,6 +503,8 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
416
503
 
417
504
  file_capability = get_file_query_capability(api_url, api_key)
418
505
  services["File"] = file_capability["status"]
506
+ system_capability = get_system_query_capability(api_url, api_key)
507
+ services["Systems"] = system_capability["status"]
419
508
 
420
509
  return {
421
510
  "server_reachable": True,
@@ -423,6 +512,8 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
423
512
  "services": services,
424
513
  "file_query_endpoint": file_capability["file_query_endpoint"],
425
514
  "elasticsearch_available": file_capability["elasticsearch_available"],
515
+ "system_query_endpoint": system_capability["system_query_endpoint"],
516
+ "materialized_search_available": system_capability["materialized_search_available"],
426
517
  "platform": platform,
427
518
  }
428
519
 
@@ -473,6 +564,8 @@ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
473
564
  platform = stored_platform
474
565
  file_query_endpoint: Optional[str] = None
475
566
  elasticsearch_available: Optional[bool] = None
567
+ system_query_endpoint: Optional[str] = None
568
+ materialized_search_available: Optional[bool] = None
476
569
 
477
570
  if not skip_health and logged_in and isinstance(api_url, str) and api_url != "Not configured":
478
571
  status = check_service_status(api_url, api_key)
@@ -482,6 +575,8 @@ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
482
575
  platform = status["platform"]
483
576
  file_query_endpoint = status.get("file_query_endpoint")
484
577
  elasticsearch_available = status.get("elasticsearch_available")
578
+ system_query_endpoint = status.get("system_query_endpoint")
579
+ materialized_search_available = status.get("materialized_search_available")
485
580
 
486
581
  info: Dict[str, Any] = {
487
582
  "api_url": api_url,
@@ -497,6 +592,10 @@ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
497
592
  info["file_query_endpoint"] = file_query_endpoint
498
593
  if elasticsearch_available is not None:
499
594
  info["elasticsearch_available"] = elasticsearch_available
595
+ if system_query_endpoint is not None:
596
+ info["system_query_endpoint"] = system_query_endpoint
597
+ if materialized_search_available is not None:
598
+ info["materialized_search_available"] = materialized_search_available
500
599
 
501
600
  if services is not None:
502
601
  info["services"] = services
@@ -11,7 +11,14 @@ import questionary
11
11
  from .utils import ExitCodes
12
12
 
13
13
  SKILL_NAME = "slcli"
14
- SKILL_CHOICES = ["slcli", "systemlink-webapp", "systemlink-notebook"]
14
+ _FALLBACK_SKILL_CHOICES = [
15
+ "nipkg-file-package",
16
+ "slcli",
17
+ "systemlink-job-debugging",
18
+ "systemlink-notebook",
19
+ "systemlink-python-test",
20
+ "systemlink-webapp",
21
+ ]
15
22
 
16
23
  # Mapping of client name -> (personal skills dir, project subdir relative to repo root)
17
24
  # personal dir uses Path.home() so it's always resolved at call time via _personal_dir().
@@ -23,6 +30,45 @@ _CLIENT_TABLE: Dict[str, Tuple[str, str]] = {
23
30
  CLIENT_CHOICES = list(_CLIENT_TABLE.keys())
24
31
 
25
32
 
33
+ def _skills_dir_candidates() -> List[Path]:
34
+ """Return candidate bundled skills directories for source and frozen layouts."""
35
+ candidates: List[Path] = []
36
+
37
+ meipass = getattr(sys, "_MEIPASS", None)
38
+ if meipass:
39
+ candidates.append(Path(meipass) / "skills")
40
+
41
+ if getattr(sys, "frozen", False):
42
+ candidates.append(Path(sys.executable).resolve().parent / "skills")
43
+
44
+ candidates.append(Path(__file__).resolve().parent / "skills")
45
+ return candidates
46
+
47
+
48
+ def _skill_names_in_directory(directory: Path) -> List[str]:
49
+ """Return sorted bundled skill names for a skills directory."""
50
+ if not directory.exists():
51
+ return []
52
+
53
+ return sorted(
54
+ child.name
55
+ for child in directory.iterdir()
56
+ if child.is_dir() and (child / "SKILL.md").exists()
57
+ )
58
+
59
+
60
+ def _discover_skill_choices() -> List[str]:
61
+ """Discover bundled skills from the current installation layout."""
62
+ for candidate in _skills_dir_candidates():
63
+ names = _skill_names_in_directory(candidate)
64
+ if names:
65
+ return names
66
+ return list(_FALLBACK_SKILL_CHOICES)
67
+
68
+
69
+ SKILL_CHOICES = _discover_skill_choices()
70
+
71
+
26
72
  def _personal_dir(client: str) -> Path:
27
73
  """Return the resolved personal skills directory for a client."""
28
74
  return Path(_CLIENT_TABLE[client][0]).expanduser()
@@ -57,24 +103,8 @@ def _find_bundled_skills_dir() -> Path:
57
103
  Raises:
58
104
  FileNotFoundError: If the skills directory cannot be found.
59
105
  """
60
- candidates: List[Path] = []
61
-
62
- # PyInstaller onefile mode
63
- meipass = getattr(sys, "_MEIPASS", None)
64
- if meipass:
65
- candidates.append(Path(meipass) / "skills")
66
-
67
- # PyInstaller onedir / frozen executable
68
- if getattr(sys, "frozen", False):
69
- candidates.append(Path(sys.executable).resolve().parent / "skills")
70
-
71
- # pip install / development: skills/ bundled inside the slcli package
72
- candidates.append(Path(__file__).resolve().parent / "skills")
73
-
74
- for candidate in candidates:
75
- if candidate.exists() and any(
76
- (candidate / name / "SKILL.md").exists() for name in SKILL_CHOICES
77
- ):
106
+ for candidate in _skills_dir_candidates():
107
+ if _skill_names_in_directory(candidate):
78
108
  return candidate
79
109
 
80
110
  raise FileNotFoundError(
@@ -178,7 +208,7 @@ def register_skill_commands(cli: Any) -> None:
178
208
  "-k",
179
209
  type=click.Choice(SKILL_CHOICES + ["all"], case_sensitive=False),
180
210
  default=None,
181
- help="Skill to install (slcli, systemlink-webapp, systemlink-notebook, or all).",
211
+ help=f"Skill to install ({', '.join(SKILL_CHOICES)}, or all).",
182
212
  )
183
213
  @click.option(
184
214
  "--client",
@@ -207,7 +237,7 @@ def register_skill_commands(cli: Any) -> None:
207
237
  """Install agent skills for AI coding assistants.
208
238
 
209
239
  Copies bundled skills into the skills directory of one or more AI clients.
210
- Available skills: slcli, systemlink-webapp, systemlink-notebook.
240
+ Available skills are shown in `--help` and prompted interactively.
211
241
  Supported clients and their skill locations:
212
242
 
213
243
  \b