shortcuts-playground 1.2.1
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.
- package/README.md +54 -0
- package/agents/shortcut-builder.md +172 -0
- package/agents/shortcut-remixer.md +239 -0
- package/commands/build-shortcut.md +32 -0
- package/commands/remix-shortcut.md +34 -0
- package/opencode.json +30 -0
- package/package.json +47 -0
- package/plugin/validator.ts +179 -0
- package/skills/shortcuts-playground/ACTIONS.md +713 -0
- package/skills/shortcuts-playground/APPINTENTS.md +2760 -0
- package/skills/shortcuts-playground/AUTOMATION_TRIGGERS.md +133 -0
- package/skills/shortcuts-playground/BEST_PRACTICES.md +357 -0
- package/skills/shortcuts-playground/CHANGELOG.md +585 -0
- package/skills/shortcuts-playground/CONTROL_FLOW.md +777 -0
- package/skills/shortcuts-playground/DATE_TIME.md +75 -0
- package/skills/shortcuts-playground/EXAMPLES.md +738 -0
- package/skills/shortcuts-playground/FILTERS.md +697 -0
- package/skills/shortcuts-playground/HEALTHKIT.md +317 -0
- package/skills/shortcuts-playground/ICONS_AND_COLORS.md +89 -0
- package/skills/shortcuts-playground/JAVASCRIPT_WEBPAGE.md +51 -0
- package/skills/shortcuts-playground/PARAMETER_TYPES.md +1117 -0
- package/skills/shortcuts-playground/PLIST_FORMAT.md +296 -0
- package/skills/shortcuts-playground/README.md +77 -0
- package/skills/shortcuts-playground/SKILL.md +459 -0
- package/skills/shortcuts-playground/THIRD_PARTY_ACTIONS.md +77 -0
- package/skills/shortcuts-playground/TOOLKIT_SNAPSHOT.md +62 -0
- package/skills/shortcuts-playground/URL_SCHEMES.md +59 -0
- package/skills/shortcuts-playground/VARIABLES.md +569 -0
- package/skills/shortcuts-playground/assets/shortcuts-small.svg +14 -0
- package/skills/shortcuts-playground/assets/shortcuts.png +0 -0
- package/skills/shortcuts-playground/data/healthkit-ios26.2-reference.json +2603 -0
- package/skills/shortcuts-playground/data/macos27-shortpy-grounding.json +4085 -0
- package/skills/shortcuts-playground/data/macos27-workflow-trigger-samples.json +1 -0
- package/skills/shortcuts-playground/data/shortcuts-glyph-synonyms.json +5102 -0
- package/skills/shortcuts-playground/data/shortcuts-icon-colors.json +107 -0
- package/skills/shortcuts-playground/data/shortcuts-official-glyph-mapping.json +509 -0
- package/skills/shortcuts-playground/data/toolkit-v63-tool-ids.json +1806 -0
- package/skills/shortcuts-playground/data/toolkit-v78-first-party-enum-cases.json +1 -0
- package/skills/shortcuts-playground/data/toolkit-v78-first-party-parameter-keys.json +1 -0
- package/skills/shortcuts-playground/data/toolkit-v78-ios27-tool-ids.json +1222 -0
- package/skills/shortcuts-playground/data/toolkit-v78-tool-ids.json +2745 -0
- package/skills/shortcuts-playground/data/toolkit-v78-trigger-parameter-keys.json +1051 -0
- package/skills/shortcuts-playground/golden-shortcuts/index.jsonl +19 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/1be4dde95b794253bf82438e201b33e7.xml +174 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/2e0fb675e45948aaacee7e534f910492.xml +1016 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/332c12a0060043b388b22b806be7ab58.xml +1924 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/3dd4ee24e43f464f92adaa70a0311eaa.xml +1177 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/4d102301c6e646faa7a8a221f4f4ec98.xml +321 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/51cc4e26d1044893a0c3f2f3630cf2d2.xml +1933 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/623e7f1ca5f948e2bd53811fec63e544.xml +136 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/6a18b76843ac45c384ac3400f3740997.xml +1791 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/71f0cacb0f604b399b76c5dcb7286e7c.xml +2228 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/807525ed9f974829bc8494defac923a8.xml +259 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/8ab0d39826ab4249be58672763caa3ba.xml +420 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/91c45fd3fd3b427897d9ba485efb1227.xml +800 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/97be626bb25c41709d175646a7f6d8f2.xml +424 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/ae59e10d409348f9bd33894f03f9beb4.xml +568 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/afa83b6be811483b9c32189c41eb9312.xml +391 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/cfdb083b857e4ac189629fa386d27cdc.xml +193 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/e01cebe192d64b2fbca80204d03d92ab.xml +111 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/ef669bcf8bad489d9ef4b88bfaf5772f.xml +780 -0
- package/skills/shortcuts-playground/golden-shortcuts/xml/f44f5caf5e3e48d4817e73af450c4404.xml +570 -0
- package/skills/shortcuts-playground/scripts/generate_healthkit_reference.py +394 -0
- package/skills/shortcuts-playground/scripts/lookup_action_grounding.py +1164 -0
- package/skills/shortcuts-playground/scripts/select_shortcut_icon_color.py +597 -0
- package/skills/shortcuts-playground/scripts/test_random_mixed_shortcuts.py +1067 -0
- package/skills/shortcuts-playground/scripts/test_wiring_regressions.py +2247 -0
- package/skills/shortcuts-playground/scripts/validate_shortcut.py +4526 -0
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Look up reviewed Apple-derived Shortcuts grounding metadata.
|
|
3
|
+
|
|
4
|
+
This helper reads the packaged static macOS 27 Shortpy catalog plus compact
|
|
5
|
+
ToolKit v78 first-party parameter-key, enum-case, trigger, and exported
|
|
6
|
+
workflow-trigger snapshots. It never reads the user's live Shortcuts databases
|
|
7
|
+
and never calls private Apple frameworks.
|
|
8
|
+
Use it as an authoring aid when a macOS 27 action or Apple Shortpy function
|
|
9
|
+
name needs additional grounding beyond the markdown references.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import platform
|
|
18
|
+
import subprocess
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
TARGET_MACOS_ENV_VARS = (
|
|
24
|
+
"SHORTCUTS_PLAYGROUND_TARGET_MACOS",
|
|
25
|
+
"CLAUDE_PLUGIN_OPTION_TARGET_MACOS",
|
|
26
|
+
)
|
|
27
|
+
TARGET_PLATFORM_ENV_VARS = (
|
|
28
|
+
"SHORTCUTS_PLAYGROUND_TARGET_PLATFORM",
|
|
29
|
+
"CLAUDE_PLUGIN_OPTION_TARGET_PLATFORM",
|
|
30
|
+
)
|
|
31
|
+
TOOLKIT_SNAPSHOT_MIN_MACOS_MAJOR = {
|
|
32
|
+
"toolkit-v78": 27,
|
|
33
|
+
"toolkit-v78-ios27": 27,
|
|
34
|
+
}
|
|
35
|
+
PARAMETER_CATALOG_MIN_MACOS_MAJOR = 27
|
|
36
|
+
TRIGGER_CATALOG_MIN_MACOS_MAJOR = 27
|
|
37
|
+
WORKFLOW_TRIGGER_CATALOG_MIN_MACOS_MAJOR = 27
|
|
38
|
+
DEPRECATED_IDENTIFIER_NOTES = {
|
|
39
|
+
"is.workflow.actions.getonscreencontent": (
|
|
40
|
+
"Deprecated in ToolKit v78; use is.workflow.actions.getonscreencontext "
|
|
41
|
+
"for new Get What's On Screen shortcuts."
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def toolkit_snapshot_min_macos_major(version: str | None) -> int | None:
|
|
47
|
+
if not isinstance(version, str) or not version:
|
|
48
|
+
return None
|
|
49
|
+
if version in TOOLKIT_SNAPSHOT_MIN_MACOS_MAJOR:
|
|
50
|
+
return TOOLKIT_SNAPSHOT_MIN_MACOS_MAJOR[version]
|
|
51
|
+
import re
|
|
52
|
+
|
|
53
|
+
match = re.search(r"v(\d+)", version)
|
|
54
|
+
if match and int(match.group(1)) >= 78:
|
|
55
|
+
return 27
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def skill_dir() -> Path:
|
|
60
|
+
return Path(__file__).resolve().parents[1]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def catalog_path(base: Path | None = None) -> Path:
|
|
64
|
+
return (base or skill_dir()) / "data/macos27-shortpy-grounding.json"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parameter_catalog_path(base: Path | None = None) -> Path:
|
|
68
|
+
return (base or skill_dir()) / "data/toolkit-v78-first-party-parameter-keys.json"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def enum_catalog_path(base: Path | None = None) -> Path:
|
|
72
|
+
return (base or skill_dir()) / "data/toolkit-v78-first-party-enum-cases.json"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def trigger_catalog_path(base: Path | None = None) -> Path:
|
|
76
|
+
return (base or skill_dir()) / "data/toolkit-v78-trigger-parameter-keys.json"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def workflow_trigger_catalog_path(base: Path | None = None) -> Path:
|
|
80
|
+
return (base or skill_dir()) / "data/macos27-workflow-trigger-samples.json"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_catalog(base: Path | None = None) -> dict[str, Any]:
|
|
84
|
+
path = catalog_path(base)
|
|
85
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
86
|
+
return json.load(handle)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_parameter_catalog(base: Path | None = None) -> dict[str, Any]:
|
|
90
|
+
path = parameter_catalog_path(base)
|
|
91
|
+
if not path.exists():
|
|
92
|
+
return {"tools": {}}
|
|
93
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
94
|
+
return json.load(handle)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_enum_catalog(base: Path | None = None) -> dict[str, Any]:
|
|
98
|
+
path = enum_catalog_path(base)
|
|
99
|
+
if not path.exists():
|
|
100
|
+
return {"types": {}}
|
|
101
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
102
|
+
return json.load(handle)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def load_trigger_catalog(base: Path | None = None) -> dict[str, Any]:
|
|
106
|
+
path = trigger_catalog_path(base)
|
|
107
|
+
if not path.exists():
|
|
108
|
+
return {"triggers": {}}
|
|
109
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
110
|
+
return json.load(handle)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_workflow_trigger_catalog(base: Path | None = None) -> dict[str, Any]:
|
|
114
|
+
path = workflow_trigger_catalog_path(base)
|
|
115
|
+
if not path.exists():
|
|
116
|
+
return {"triggers": {}}
|
|
117
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
118
|
+
return json.load(handle)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_identifier_min_macos(base: Path | None = None) -> dict[str, int | None]:
|
|
122
|
+
"""Return the earliest packaged snapshot availability for each identifier."""
|
|
123
|
+
|
|
124
|
+
data_dir = (base or skill_dir()) / "data"
|
|
125
|
+
availability: dict[str, int | None] = {}
|
|
126
|
+
for path in sorted(data_dir.glob("toolkit-v*-tool-ids.json")):
|
|
127
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
128
|
+
payload = json.load(handle)
|
|
129
|
+
version = payload.get("version") or path.stem.replace("-tool-ids", "")
|
|
130
|
+
min_macos = toolkit_snapshot_min_macos_major(version)
|
|
131
|
+
ids = set(payload.get("ids") or [])
|
|
132
|
+
ids.update(payload.get("control_flow_exceptions_missing_from_tools_table") or [])
|
|
133
|
+
for identifier in ids:
|
|
134
|
+
if identifier not in availability:
|
|
135
|
+
availability[identifier] = min_macos
|
|
136
|
+
elif availability[identifier] is not None and (
|
|
137
|
+
min_macos is None or min_macos < availability[identifier]
|
|
138
|
+
):
|
|
139
|
+
availability[identifier] = min_macos
|
|
140
|
+
return availability
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def host_macos_major() -> int | None:
|
|
144
|
+
if platform.system() != "Darwin":
|
|
145
|
+
return None
|
|
146
|
+
try:
|
|
147
|
+
result = subprocess.run(
|
|
148
|
+
["sw_vers", "-productVersion"],
|
|
149
|
+
capture_output=True,
|
|
150
|
+
text=True,
|
|
151
|
+
timeout=2,
|
|
152
|
+
check=False,
|
|
153
|
+
)
|
|
154
|
+
except (OSError, subprocess.SubprocessError):
|
|
155
|
+
return None
|
|
156
|
+
if result.returncode != 0:
|
|
157
|
+
return None
|
|
158
|
+
major = result.stdout.strip().split(".", 1)[0]
|
|
159
|
+
return int(major) if major.isdigit() else None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def resolve_target_macos(value: str | None) -> int | None:
|
|
163
|
+
raw = value
|
|
164
|
+
if raw is None:
|
|
165
|
+
for env_name in TARGET_MACOS_ENV_VARS:
|
|
166
|
+
raw = os.environ.get(env_name)
|
|
167
|
+
if raw:
|
|
168
|
+
break
|
|
169
|
+
if raw is None or raw == "" or raw.lower() == "auto":
|
|
170
|
+
detected = host_macos_major()
|
|
171
|
+
return detected if detected is not None else 26
|
|
172
|
+
if raw.lower() in {"latest", "all"}:
|
|
173
|
+
return None
|
|
174
|
+
if raw.isdigit():
|
|
175
|
+
return int(raw)
|
|
176
|
+
raise ValueError(f"Invalid macOS target: {raw!r}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def resolve_target_platform(value: str | None) -> str | None:
|
|
180
|
+
raw = value
|
|
181
|
+
if raw is None:
|
|
182
|
+
for env_name in TARGET_PLATFORM_ENV_VARS:
|
|
183
|
+
raw = os.environ.get(env_name)
|
|
184
|
+
if raw:
|
|
185
|
+
break
|
|
186
|
+
if raw is None:
|
|
187
|
+
raw = "macos"
|
|
188
|
+
normalized = raw.strip().lower().replace("_", "-")
|
|
189
|
+
if normalized in {"", "auto", "host", "mac", "macos", "mac-os"}:
|
|
190
|
+
return "macos"
|
|
191
|
+
if normalized in {"ios", "ipados", "iphone", "ipad"}:
|
|
192
|
+
return "ios"
|
|
193
|
+
if normalized in {"latest", "all", "any"}:
|
|
194
|
+
return None
|
|
195
|
+
raise ValueError(f"Invalid target platform: {raw!r}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def normalized_identifier(value: str) -> str:
|
|
199
|
+
if value.startswith("is.workflow.actions."):
|
|
200
|
+
return value
|
|
201
|
+
return f"is.workflow.actions.{value}"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def find_by_identifier(catalog: dict[str, Any], value: str) -> tuple[str, dict[str, Any]] | None:
|
|
205
|
+
tools = catalog.get("tools") or {}
|
|
206
|
+
structural = catalog.get("structuralActions") or {}
|
|
207
|
+
candidates = {**tools, **structural}
|
|
208
|
+
for key in (value, normalized_identifier(value)):
|
|
209
|
+
if key in candidates:
|
|
210
|
+
return key, candidates[key]
|
|
211
|
+
suffix = value.removeprefix("is.workflow.actions.")
|
|
212
|
+
matches = [
|
|
213
|
+
(identifier, entry)
|
|
214
|
+
for identifier, entry in candidates.items()
|
|
215
|
+
if identifier.endswith(f".{suffix}") or identifier.rsplit(".", 1)[-1] == suffix
|
|
216
|
+
]
|
|
217
|
+
return matches[0] if len(matches) == 1 else None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def find_by_python_name(catalog: dict[str, Any], value: str) -> tuple[str, dict[str, Any]] | None:
|
|
221
|
+
lookup = catalog.get("pythonLookup") or {}
|
|
222
|
+
if value in lookup:
|
|
223
|
+
entry = lookup[value]
|
|
224
|
+
identifier = entry.get("wfIdentifier")
|
|
225
|
+
if identifier:
|
|
226
|
+
found = find_by_identifier(catalog, identifier)
|
|
227
|
+
return found or (identifier, entry)
|
|
228
|
+
for identifier, entry in (catalog.get("tools") or {}).items():
|
|
229
|
+
if entry.get("pythonName") == value:
|
|
230
|
+
return identifier, entry
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def parameter_entry_to_grounding(identifier: str, entry: dict[str, Any]) -> dict[str, Any]:
|
|
235
|
+
return {
|
|
236
|
+
"status": "toolkit-parameter-summary",
|
|
237
|
+
"name": entry.get("displayName") or identifier,
|
|
238
|
+
"pythonName": entry.get("pythonName"),
|
|
239
|
+
"summary": (
|
|
240
|
+
"ToolKit v78 parameter-key summary. This proves the action exists "
|
|
241
|
+
"and names its parameters, but it is not a full authored shortcut sample."
|
|
242
|
+
),
|
|
243
|
+
"toolkitPlatforms": entry.get("platforms") or [],
|
|
244
|
+
"toolkitToolType": entry.get("toolType"),
|
|
245
|
+
"toolkitParameterSummary": {
|
|
246
|
+
"parameterCount": entry.get("parameterCount"),
|
|
247
|
+
"parameters": entry.get("parameters") or [],
|
|
248
|
+
},
|
|
249
|
+
"parameters": [],
|
|
250
|
+
"sourceFunctions": {},
|
|
251
|
+
"sampleShortcuts": [],
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def trigger_entry_to_grounding(identifier: str, entry: dict[str, Any]) -> dict[str, Any]:
|
|
256
|
+
return {
|
|
257
|
+
"status": "toolkit-trigger-summary",
|
|
258
|
+
"minimumMacOSMajor": TRIGGER_CATALOG_MIN_MACOS_MAJOR,
|
|
259
|
+
"name": entry.get("displayName") or identifier,
|
|
260
|
+
"pythonName": entry.get("pythonName"),
|
|
261
|
+
"summary": (
|
|
262
|
+
"ToolKit v78 automation-trigger metadata. This proves the trigger "
|
|
263
|
+
"exists and names its parameters. When present, workflowTriggerSample "
|
|
264
|
+
"shows the exported WFWorkflowTriggers plist shape for OS 27 shortcut XML."
|
|
265
|
+
),
|
|
266
|
+
"toolkitPlatforms": entry.get("platforms") or [],
|
|
267
|
+
"toolkitTriggerSummary": {
|
|
268
|
+
"parameterCount": entry.get("parameterCount"),
|
|
269
|
+
"parameters": entry.get("parameters") or [],
|
|
270
|
+
"outputTypeIdentifiers": entry.get("outputTypeIdentifiers") or [],
|
|
271
|
+
},
|
|
272
|
+
"parameters": [],
|
|
273
|
+
"sourceFunctions": {},
|
|
274
|
+
"sampleShortcuts": [],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def augment_with_parameter_summary(
|
|
279
|
+
identifier: str,
|
|
280
|
+
entry: dict[str, Any],
|
|
281
|
+
parameter_catalog: dict[str, Any],
|
|
282
|
+
) -> dict[str, Any]:
|
|
283
|
+
toolkit_entry = (parameter_catalog.get("tools") or {}).get(identifier)
|
|
284
|
+
if not isinstance(toolkit_entry, dict):
|
|
285
|
+
return entry
|
|
286
|
+
out = dict(entry)
|
|
287
|
+
out["toolkitPlatforms"] = toolkit_entry.get("platforms") or []
|
|
288
|
+
out["toolkitToolType"] = toolkit_entry.get("toolType")
|
|
289
|
+
out["toolkitParameterSummary"] = {
|
|
290
|
+
"parameterCount": toolkit_entry.get("parameterCount"),
|
|
291
|
+
"parameters": toolkit_entry.get("parameters") or [],
|
|
292
|
+
}
|
|
293
|
+
return out
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def toolkit_parameter_type_names(parameter: dict[str, Any]) -> list[str]:
|
|
297
|
+
type_names = parameter.get("typePythonNames")
|
|
298
|
+
if isinstance(type_names, list):
|
|
299
|
+
return [str(name) for name in type_names if name]
|
|
300
|
+
single = parameter.get("typePythonName")
|
|
301
|
+
return [str(single)] if single else []
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def toolkit_parameter_matches_target(
|
|
305
|
+
parameter: dict[str, Any],
|
|
306
|
+
target_platform: str | None,
|
|
307
|
+
) -> bool:
|
|
308
|
+
platforms = parameter.get("platforms")
|
|
309
|
+
if not isinstance(platforms, list) or not platforms:
|
|
310
|
+
return True
|
|
311
|
+
return platform_matches_target(
|
|
312
|
+
[str(platform) for platform in platforms if platform],
|
|
313
|
+
target_platform,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def toolkit_parameter_type_names_for_target(
|
|
318
|
+
parameter: dict[str, Any],
|
|
319
|
+
target_platform: str | None,
|
|
320
|
+
) -> list[str]:
|
|
321
|
+
if target_platform is not None:
|
|
322
|
+
by_platform = parameter.get("typePythonNamesByPlatform")
|
|
323
|
+
if isinstance(by_platform, dict):
|
|
324
|
+
names: list[str] = []
|
|
325
|
+
for platform, type_names in by_platform.items():
|
|
326
|
+
if not isinstance(platform, str) or not platform_matches_target(
|
|
327
|
+
[platform],
|
|
328
|
+
target_platform,
|
|
329
|
+
):
|
|
330
|
+
continue
|
|
331
|
+
if isinstance(type_names, list):
|
|
332
|
+
names.extend(str(name) for name in type_names if name)
|
|
333
|
+
if names:
|
|
334
|
+
return sorted(set(names))
|
|
335
|
+
return toolkit_parameter_type_names(parameter)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def filter_toolkit_parameter_for_target(
|
|
339
|
+
parameter: dict[str, Any],
|
|
340
|
+
target_platform: str | None,
|
|
341
|
+
) -> dict[str, Any] | None:
|
|
342
|
+
if not toolkit_parameter_matches_target(parameter, target_platform):
|
|
343
|
+
return None
|
|
344
|
+
out = dict(parameter)
|
|
345
|
+
if target_platform is not None:
|
|
346
|
+
type_names = toolkit_parameter_type_names_for_target(parameter, target_platform)
|
|
347
|
+
if len(type_names) == 1:
|
|
348
|
+
out["typePythonName"] = type_names[0]
|
|
349
|
+
out.pop("typePythonNames", None)
|
|
350
|
+
elif type_names:
|
|
351
|
+
out.pop("typePythonName", None)
|
|
352
|
+
out["typePythonNames"] = type_names
|
|
353
|
+
return out
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def filter_toolkit_parameter_summary_for_target(
|
|
357
|
+
summary: dict[str, Any],
|
|
358
|
+
target_platform: str | None,
|
|
359
|
+
) -> dict[str, Any]:
|
|
360
|
+
parameters = summary.get("parameters")
|
|
361
|
+
if not isinstance(parameters, list):
|
|
362
|
+
return summary
|
|
363
|
+
filtered: list[dict[str, Any]] = []
|
|
364
|
+
for parameter in parameters:
|
|
365
|
+
if not isinstance(parameter, dict):
|
|
366
|
+
continue
|
|
367
|
+
filtered_parameter = filter_toolkit_parameter_for_target(
|
|
368
|
+
parameter,
|
|
369
|
+
target_platform,
|
|
370
|
+
)
|
|
371
|
+
if filtered_parameter is not None:
|
|
372
|
+
filtered.append(filtered_parameter)
|
|
373
|
+
out = dict(summary)
|
|
374
|
+
out["parameters"] = filtered
|
|
375
|
+
out["parameterCount"] = len(filtered)
|
|
376
|
+
return out
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def filter_entry_for_target_platform(
|
|
380
|
+
entry: dict[str, Any],
|
|
381
|
+
target_platform: str | None,
|
|
382
|
+
) -> dict[str, Any]:
|
|
383
|
+
summary = entry.get("toolkitParameterSummary")
|
|
384
|
+
if not isinstance(summary, dict):
|
|
385
|
+
return entry
|
|
386
|
+
out = dict(entry)
|
|
387
|
+
out["toolkitParameterSummary"] = filter_toolkit_parameter_summary_for_target(
|
|
388
|
+
summary,
|
|
389
|
+
target_platform,
|
|
390
|
+
)
|
|
391
|
+
return out
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def enum_type_summary(
|
|
395
|
+
type_python_name: str,
|
|
396
|
+
enum_catalog: dict[str, Any],
|
|
397
|
+
) -> dict[str, Any] | None:
|
|
398
|
+
enum_entry = (enum_catalog.get("types") or {}).get(type_python_name)
|
|
399
|
+
if not isinstance(enum_entry, dict):
|
|
400
|
+
return None
|
|
401
|
+
cases = enum_entry.get("cases") or []
|
|
402
|
+
if not cases:
|
|
403
|
+
return None
|
|
404
|
+
return {
|
|
405
|
+
"typePythonName": type_python_name,
|
|
406
|
+
"displayNames": enum_entry.get("displayNames") or [],
|
|
407
|
+
"platforms": enum_entry.get("platforms") or [],
|
|
408
|
+
"caseCount": enum_entry.get("caseCount") or len(cases),
|
|
409
|
+
"cases": cases,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def augment_parameter_with_enum_cases(
|
|
414
|
+
parameter: dict[str, Any],
|
|
415
|
+
enum_catalog: dict[str, Any],
|
|
416
|
+
) -> dict[str, Any]:
|
|
417
|
+
enum_types = [
|
|
418
|
+
enum_type
|
|
419
|
+
for type_name in toolkit_parameter_type_names(parameter)
|
|
420
|
+
if (enum_type := enum_type_summary(type_name, enum_catalog)) is not None
|
|
421
|
+
]
|
|
422
|
+
if not enum_types:
|
|
423
|
+
return parameter
|
|
424
|
+
out = dict(parameter)
|
|
425
|
+
out["enumTypes"] = enum_types
|
|
426
|
+
if len(enum_types) == 1:
|
|
427
|
+
out["enumCases"] = enum_types[0]["cases"]
|
|
428
|
+
out["enumCaseCount"] = enum_types[0]["caseCount"]
|
|
429
|
+
out["enumDisplayNames"] = enum_types[0]["displayNames"]
|
|
430
|
+
return out
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def augment_summary_parameters_with_enum_cases(
|
|
434
|
+
summary: dict[str, Any],
|
|
435
|
+
enum_catalog: dict[str, Any],
|
|
436
|
+
) -> dict[str, Any]:
|
|
437
|
+
parameters = summary.get("parameters")
|
|
438
|
+
if not isinstance(parameters, list):
|
|
439
|
+
return summary
|
|
440
|
+
out = dict(summary)
|
|
441
|
+
out["parameters"] = [
|
|
442
|
+
augment_parameter_with_enum_cases(parameter, enum_catalog)
|
|
443
|
+
if isinstance(parameter, dict)
|
|
444
|
+
else parameter
|
|
445
|
+
for parameter in parameters
|
|
446
|
+
]
|
|
447
|
+
return out
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def augment_with_enum_cases(
|
|
451
|
+
entry: dict[str, Any],
|
|
452
|
+
enum_catalog: dict[str, Any],
|
|
453
|
+
) -> dict[str, Any]:
|
|
454
|
+
out = dict(entry)
|
|
455
|
+
parameter_summary = out.get("toolkitParameterSummary")
|
|
456
|
+
if isinstance(parameter_summary, dict):
|
|
457
|
+
out["toolkitParameterSummary"] = augment_summary_parameters_with_enum_cases(
|
|
458
|
+
parameter_summary,
|
|
459
|
+
enum_catalog,
|
|
460
|
+
)
|
|
461
|
+
trigger_summary = out.get("toolkitTriggerSummary")
|
|
462
|
+
if isinstance(trigger_summary, dict):
|
|
463
|
+
out["toolkitTriggerSummary"] = augment_summary_parameters_with_enum_cases(
|
|
464
|
+
trigger_summary,
|
|
465
|
+
enum_catalog,
|
|
466
|
+
)
|
|
467
|
+
return out
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def augment_with_workflow_trigger_sample(
|
|
471
|
+
identifier: str,
|
|
472
|
+
entry: dict[str, Any],
|
|
473
|
+
workflow_trigger_catalog: dict[str, Any],
|
|
474
|
+
) -> dict[str, Any]:
|
|
475
|
+
sample = (workflow_trigger_catalog.get("triggers") or {}).get(identifier)
|
|
476
|
+
if not isinstance(sample, dict):
|
|
477
|
+
return entry
|
|
478
|
+
out = dict(entry)
|
|
479
|
+
out["workflowTriggerSample"] = sample
|
|
480
|
+
return out
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def find_parameter_by_identifier(
|
|
484
|
+
parameter_catalog: dict[str, Any],
|
|
485
|
+
value: str,
|
|
486
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
487
|
+
tools = parameter_catalog.get("tools") or {}
|
|
488
|
+
for key in (value, normalized_identifier(value)):
|
|
489
|
+
entry = tools.get(key)
|
|
490
|
+
if isinstance(entry, dict):
|
|
491
|
+
return key, parameter_entry_to_grounding(key, entry)
|
|
492
|
+
suffix = value.removeprefix("is.workflow.actions.")
|
|
493
|
+
matches = [
|
|
494
|
+
(identifier, parameter_entry_to_grounding(identifier, entry))
|
|
495
|
+
for identifier, entry in tools.items()
|
|
496
|
+
if identifier.endswith(f".{suffix}") or identifier.rsplit(".", 1)[-1] == suffix
|
|
497
|
+
]
|
|
498
|
+
return matches[0] if len(matches) == 1 else None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def find_parameter_by_python_name(
|
|
502
|
+
parameter_catalog: dict[str, Any],
|
|
503
|
+
value: str,
|
|
504
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
505
|
+
tools = parameter_catalog.get("tools") or {}
|
|
506
|
+
matches = [
|
|
507
|
+
(identifier, parameter_entry_to_grounding(identifier, entry))
|
|
508
|
+
for identifier, entry in tools.items()
|
|
509
|
+
if entry.get("pythonName") == value
|
|
510
|
+
]
|
|
511
|
+
return matches[0] if len(matches) == 1 else None
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def find_trigger_by_identifier(
|
|
515
|
+
trigger_catalog: dict[str, Any],
|
|
516
|
+
value: str,
|
|
517
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
518
|
+
triggers = trigger_catalog.get("triggers") or {}
|
|
519
|
+
entry = triggers.get(value)
|
|
520
|
+
if isinstance(entry, dict):
|
|
521
|
+
return value, trigger_entry_to_grounding(value, entry)
|
|
522
|
+
suffix = value.removeprefix("com.apple.shortcuts.")
|
|
523
|
+
matches = [
|
|
524
|
+
(identifier, trigger_entry_to_grounding(identifier, entry))
|
|
525
|
+
for identifier, entry in triggers.items()
|
|
526
|
+
if identifier.endswith(suffix) or identifier.rsplit(".", 1)[-1] == suffix
|
|
527
|
+
]
|
|
528
|
+
return matches[0] if len(matches) == 1 else None
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def find_trigger_by_python_name(
|
|
532
|
+
trigger_catalog: dict[str, Any],
|
|
533
|
+
value: str,
|
|
534
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
535
|
+
triggers = trigger_catalog.get("triggers") or {}
|
|
536
|
+
matches = [
|
|
537
|
+
(identifier, trigger_entry_to_grounding(identifier, entry))
|
|
538
|
+
for identifier, entry in triggers.items()
|
|
539
|
+
if entry.get("pythonName") == value
|
|
540
|
+
]
|
|
541
|
+
return matches[0] if len(matches) == 1 else None
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def entry_search_text(identifier: str, entry: dict[str, Any]) -> str:
|
|
545
|
+
parts: list[str] = [
|
|
546
|
+
identifier,
|
|
547
|
+
entry.get("status") or "",
|
|
548
|
+
entry.get("name") or "",
|
|
549
|
+
entry.get("pythonName") or "",
|
|
550
|
+
entry.get("summary") or "",
|
|
551
|
+
" ".join(entry.get("keywords") or []),
|
|
552
|
+
" ".join(entry.get("sourceFunctions") or {}),
|
|
553
|
+
]
|
|
554
|
+
for parameter in entry.get("parameters") or []:
|
|
555
|
+
parts.extend(
|
|
556
|
+
[
|
|
557
|
+
parameter.get("wfKey") or "",
|
|
558
|
+
parameter.get("pythonKeyword") or "",
|
|
559
|
+
parameter.get("label") or "",
|
|
560
|
+
parameter.get("description") or "",
|
|
561
|
+
]
|
|
562
|
+
)
|
|
563
|
+
toolkit_summary = entry.get("toolkitParameterSummary") or {}
|
|
564
|
+
if isinstance(toolkit_summary, dict):
|
|
565
|
+
for parameter in toolkit_summary.get("parameters") or []:
|
|
566
|
+
if isinstance(parameter, dict):
|
|
567
|
+
parts.extend(
|
|
568
|
+
[
|
|
569
|
+
parameter.get("key") or "",
|
|
570
|
+
parameter.get("name") or "",
|
|
571
|
+
parameter.get("typePythonName") or "",
|
|
572
|
+
" ".join(toolkit_parameter_type_names(parameter)),
|
|
573
|
+
]
|
|
574
|
+
)
|
|
575
|
+
trigger_summary = entry.get("toolkitTriggerSummary") or {}
|
|
576
|
+
if isinstance(trigger_summary, dict):
|
|
577
|
+
parts.append(" ".join(trigger_summary.get("outputTypeIdentifiers") or []))
|
|
578
|
+
for parameter in trigger_summary.get("parameters") or []:
|
|
579
|
+
if isinstance(parameter, dict):
|
|
580
|
+
parts.extend(
|
|
581
|
+
[
|
|
582
|
+
parameter.get("key") or "",
|
|
583
|
+
parameter.get("name") or "",
|
|
584
|
+
parameter.get("typePythonName") or "",
|
|
585
|
+
" ".join(toolkit_parameter_type_names(parameter)),
|
|
586
|
+
]
|
|
587
|
+
)
|
|
588
|
+
workflow_trigger = entry.get("workflowTriggerSample") or {}
|
|
589
|
+
if isinstance(workflow_trigger, dict):
|
|
590
|
+
parts.append("workflow triggers exported automation header")
|
|
591
|
+
parts.extend(workflow_trigger.get("notes") or [])
|
|
592
|
+
payload = workflow_trigger.get("workflowTrigger") or {}
|
|
593
|
+
if isinstance(payload, dict):
|
|
594
|
+
parts.append(str(payload.get("WFTriggerIdentifier") or ""))
|
|
595
|
+
serialized = payload.get("WFTriggerSerializedParameters") or {}
|
|
596
|
+
if isinstance(serialized, dict):
|
|
597
|
+
parts.extend(str(key) for key in serialized)
|
|
598
|
+
return "\n".join(parts).lower()
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def search_entries(catalog: dict[str, Any], query: str, limit: int) -> list[tuple[str, dict[str, Any]]]:
|
|
602
|
+
terms = [term.lower() for term in query.split() if term]
|
|
603
|
+
tools = catalog.get("tools") or {}
|
|
604
|
+
structural = catalog.get("structuralActions") or {}
|
|
605
|
+
entries = sorted({**tools, **structural}.items())
|
|
606
|
+
if not terms:
|
|
607
|
+
return entries[:limit]
|
|
608
|
+
matches = [
|
|
609
|
+
(identifier, entry)
|
|
610
|
+
for identifier, entry in entries
|
|
611
|
+
if all(term in entry_search_text(identifier, entry) for term in terms)
|
|
612
|
+
]
|
|
613
|
+
return matches[:limit]
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def search_parameter_entries(
|
|
617
|
+
parameter_catalog: dict[str, Any],
|
|
618
|
+
query: str,
|
|
619
|
+
limit: int,
|
|
620
|
+
) -> list[tuple[str, dict[str, Any]]]:
|
|
621
|
+
terms = [term.lower() for term in query.split() if term]
|
|
622
|
+
entries = sorted((parameter_catalog.get("tools") or {}).items())
|
|
623
|
+
converted = [
|
|
624
|
+
(identifier, parameter_entry_to_grounding(identifier, entry))
|
|
625
|
+
for identifier, entry in entries
|
|
626
|
+
]
|
|
627
|
+
if not terms:
|
|
628
|
+
return converted[:limit]
|
|
629
|
+
return [
|
|
630
|
+
(identifier, entry)
|
|
631
|
+
for identifier, entry in converted
|
|
632
|
+
if all(term in entry_search_text(identifier, entry) for term in terms)
|
|
633
|
+
][:limit]
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def search_trigger_entries(
|
|
637
|
+
trigger_catalog: dict[str, Any],
|
|
638
|
+
query: str,
|
|
639
|
+
limit: int,
|
|
640
|
+
) -> list[tuple[str, dict[str, Any]]]:
|
|
641
|
+
terms = [term.lower() for term in query.split() if term]
|
|
642
|
+
entries = sorted((trigger_catalog.get("triggers") or {}).items())
|
|
643
|
+
converted = [
|
|
644
|
+
(identifier, trigger_entry_to_grounding(identifier, entry))
|
|
645
|
+
for identifier, entry in entries
|
|
646
|
+
]
|
|
647
|
+
if not terms:
|
|
648
|
+
return converted[:limit]
|
|
649
|
+
return [
|
|
650
|
+
(identifier, entry)
|
|
651
|
+
for identifier, entry in converted
|
|
652
|
+
if all(term in entry_search_text(identifier, entry) for term in terms)
|
|
653
|
+
][:limit]
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def target_note(minimum: int | None, target_macos: int | None) -> str | None:
|
|
657
|
+
if target_macos is not None and minimum is not None and target_macos < minimum:
|
|
658
|
+
return f"Requires macOS {minimum}+; target macOS is {target_macos}."
|
|
659
|
+
return None
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def deprecation_note(identifier: str) -> str | None:
|
|
663
|
+
return DEPRECATED_IDENTIFIER_NOTES.get(identifier)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def platform_availability_note(entry: dict[str, Any]) -> str | None:
|
|
667
|
+
platforms = entry.get("toolkitPlatforms") or []
|
|
668
|
+
if not isinstance(platforms, list) or not platforms:
|
|
669
|
+
return None
|
|
670
|
+
platform_text = [str(platform) for platform in platforms]
|
|
671
|
+
has_macos = any("macos" in platform.lower() for platform in platform_text)
|
|
672
|
+
has_ios = any("ios" in platform.lower() for platform in platform_text)
|
|
673
|
+
if has_ios and not has_macos:
|
|
674
|
+
return "Only observed in iOS 27 Simulator ToolKit; no macOS ToolKit row observed."
|
|
675
|
+
if has_macos and not has_ios:
|
|
676
|
+
return "Only observed in macOS 27 ToolKit; no iOS 27 Simulator ToolKit row observed."
|
|
677
|
+
return None
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def target_platform_label(target_platform: str | None) -> str:
|
|
681
|
+
if target_platform == "ios":
|
|
682
|
+
return "iOS/iPadOS"
|
|
683
|
+
if target_platform == "macos":
|
|
684
|
+
return "macOS"
|
|
685
|
+
return "all platforms"
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def platform_matches_target(platforms: list[str], target_platform: str | None) -> bool:
|
|
689
|
+
if target_platform is None:
|
|
690
|
+
return True
|
|
691
|
+
for platform in platforms:
|
|
692
|
+
normalized = platform.lower()
|
|
693
|
+
if target_platform == "ios" and "ios" in normalized:
|
|
694
|
+
return True
|
|
695
|
+
if target_platform == "macos" and "macos" in normalized:
|
|
696
|
+
return True
|
|
697
|
+
return False
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def target_platform_note(entry: dict[str, Any], target_platform: str | None) -> str | None:
|
|
701
|
+
platforms = entry.get("toolkitPlatforms") or []
|
|
702
|
+
if not isinstance(platforms, list) or not platforms:
|
|
703
|
+
return None
|
|
704
|
+
platform_text = [str(platform) for platform in platforms]
|
|
705
|
+
if platform_matches_target(platform_text, target_platform):
|
|
706
|
+
return None
|
|
707
|
+
observed = ", ".join(platform_text)
|
|
708
|
+
return (
|
|
709
|
+
f"Only observed for {observed}; target platform is "
|
|
710
|
+
f"{target_platform_label(target_platform)}."
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def has_toolkit_parameter_summary(entry: dict[str, Any]) -> bool:
|
|
715
|
+
summary = entry.get("toolkitParameterSummary")
|
|
716
|
+
if not isinstance(summary, dict):
|
|
717
|
+
return False
|
|
718
|
+
return "parameterCount" in summary or bool(summary.get("parameters"))
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def parameter_metadata_note(
|
|
722
|
+
entry: dict[str, Any],
|
|
723
|
+
target_macos: int | None,
|
|
724
|
+
action_minimum_macos: int | None,
|
|
725
|
+
) -> str | None:
|
|
726
|
+
if target_macos is None or target_macos >= PARAMETER_CATALOG_MIN_MACOS_MAJOR:
|
|
727
|
+
return None
|
|
728
|
+
if not has_toolkit_parameter_summary(entry):
|
|
729
|
+
return None
|
|
730
|
+
if action_minimum_macos is not None and action_minimum_macos > target_macos:
|
|
731
|
+
return None
|
|
732
|
+
return (
|
|
733
|
+
f"Parameter metadata is from OS {PARAMETER_CATALOG_MIN_MACOS_MAJOR} ToolKit; "
|
|
734
|
+
f"target macOS is {target_macos}."
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def trigger_metadata_note(entry: dict[str, Any], target_macos: int | None) -> str | None:
|
|
739
|
+
if target_macos is None or target_macos >= TRIGGER_CATALOG_MIN_MACOS_MAJOR:
|
|
740
|
+
return None
|
|
741
|
+
if not isinstance(entry.get("toolkitTriggerSummary"), dict):
|
|
742
|
+
return None
|
|
743
|
+
return (
|
|
744
|
+
f"Trigger metadata is from OS {TRIGGER_CATALOG_MIN_MACOS_MAJOR} ToolKit; "
|
|
745
|
+
f"target macOS is {target_macos}."
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def entry_min_macos(identifier: str, availability: dict[str, int | None]) -> int | None:
|
|
750
|
+
return availability.get(identifier)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def compact_entry(
|
|
754
|
+
identifier: str,
|
|
755
|
+
entry: dict[str, Any],
|
|
756
|
+
availability: dict[str, int | None],
|
|
757
|
+
target_macos: int | None,
|
|
758
|
+
target_platform: str | None,
|
|
759
|
+
) -> dict[str, Any]:
|
|
760
|
+
minimum = entry_min_macos(identifier, availability)
|
|
761
|
+
return {
|
|
762
|
+
"identifier": identifier,
|
|
763
|
+
"status": entry.get("status"),
|
|
764
|
+
"minimumMacOSMajor": minimum,
|
|
765
|
+
"availabilityNote": target_note(minimum, target_macos),
|
|
766
|
+
"deprecationNote": deprecation_note(identifier),
|
|
767
|
+
"parameterMetadataMinimumMacOSMajor": (
|
|
768
|
+
PARAMETER_CATALOG_MIN_MACOS_MAJOR if has_toolkit_parameter_summary(entry) else None
|
|
769
|
+
),
|
|
770
|
+
"parameterMetadataAvailabilityNote": parameter_metadata_note(
|
|
771
|
+
entry,
|
|
772
|
+
target_macos,
|
|
773
|
+
minimum,
|
|
774
|
+
),
|
|
775
|
+
"triggerMetadataMinimumMacOSMajor": (
|
|
776
|
+
TRIGGER_CATALOG_MIN_MACOS_MAJOR
|
|
777
|
+
if isinstance(entry.get("toolkitTriggerSummary"), dict)
|
|
778
|
+
else None
|
|
779
|
+
),
|
|
780
|
+
"triggerMetadataAvailabilityNote": trigger_metadata_note(entry, target_macos),
|
|
781
|
+
"platformAvailabilityNote": platform_availability_note(entry),
|
|
782
|
+
"targetPlatformAvailabilityNote": target_platform_note(entry, target_platform),
|
|
783
|
+
"name": entry.get("name"),
|
|
784
|
+
"pythonName": entry.get("pythonName"),
|
|
785
|
+
"toolRendererEmbedded": entry.get("toolRendererEmbedded"),
|
|
786
|
+
"toolRendererScriptingUtility": entry.get("toolRendererScriptingUtility"),
|
|
787
|
+
"summary": entry.get("summary"),
|
|
788
|
+
"parameters": entry.get("parameters") or [],
|
|
789
|
+
"toolkitPlatforms": entry.get("toolkitPlatforms") or [],
|
|
790
|
+
"toolkitToolType": entry.get("toolkitToolType"),
|
|
791
|
+
"toolkitParameterSummary": entry.get("toolkitParameterSummary") or {},
|
|
792
|
+
"toolkitTriggerSummary": entry.get("toolkitTriggerSummary") or {},
|
|
793
|
+
"workflowTriggerSample": entry.get("workflowTriggerSample") or {},
|
|
794
|
+
"sourceFunctions": entry.get("sourceFunctions") or {},
|
|
795
|
+
"sampleShortcuts": entry.get("sampleShortcuts") or [],
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def print_markdown_entry(
|
|
800
|
+
identifier: str,
|
|
801
|
+
entry: dict[str, Any],
|
|
802
|
+
availability: dict[str, int | None],
|
|
803
|
+
target_macos: int | None,
|
|
804
|
+
target_platform: str | None,
|
|
805
|
+
) -> None:
|
|
806
|
+
print(f"## {entry.get('name') or identifier}")
|
|
807
|
+
print()
|
|
808
|
+
print(f"- Identifier: `{identifier}`")
|
|
809
|
+
if entry.get("pythonName"):
|
|
810
|
+
print(f"- Apple Shortpy name: `{entry['pythonName']}`")
|
|
811
|
+
print(f"- Status: `{entry.get('status')}`")
|
|
812
|
+
minimum = entry_min_macos(identifier, availability)
|
|
813
|
+
if minimum is not None:
|
|
814
|
+
print(f"- Minimum target: macOS {minimum}+")
|
|
815
|
+
note = target_note(minimum, target_macos)
|
|
816
|
+
if note:
|
|
817
|
+
print(f"- Availability: {note}")
|
|
818
|
+
deprecated = deprecation_note(identifier)
|
|
819
|
+
if deprecated:
|
|
820
|
+
print(f"- Deprecation: {deprecated}")
|
|
821
|
+
parameter_note = parameter_metadata_note(entry, target_macos, minimum)
|
|
822
|
+
if parameter_note:
|
|
823
|
+
print(f"- Parameter metadata availability: {parameter_note}")
|
|
824
|
+
trigger_note = trigger_metadata_note(entry, target_macos)
|
|
825
|
+
if trigger_note:
|
|
826
|
+
print(f"- Trigger metadata availability: {trigger_note}")
|
|
827
|
+
platform_note = platform_availability_note(entry)
|
|
828
|
+
if platform_note:
|
|
829
|
+
print(f"- Platform availability: {platform_note}")
|
|
830
|
+
target_platform_message = target_platform_note(entry, target_platform)
|
|
831
|
+
if target_platform_message:
|
|
832
|
+
print(f"- Target platform availability: {target_platform_message}")
|
|
833
|
+
if entry.get("toolRendererEmbedded"):
|
|
834
|
+
flag = "ToolRenderer embedded"
|
|
835
|
+
if entry.get("toolRendererScriptingUtility"):
|
|
836
|
+
flag += ", scripting utility"
|
|
837
|
+
print(f"- Apple surface: {flag}")
|
|
838
|
+
if entry.get("summary"):
|
|
839
|
+
print(f"- Summary: {entry['summary']}")
|
|
840
|
+
samples = entry.get("sampleShortcuts") or []
|
|
841
|
+
if samples:
|
|
842
|
+
print(f"- Observed samples: {', '.join(samples)}")
|
|
843
|
+
parameters = entry.get("parameters") or []
|
|
844
|
+
if parameters:
|
|
845
|
+
print()
|
|
846
|
+
print("| WF key | Apple keyword | Required | Label |")
|
|
847
|
+
print("|--------|---------------|----------|-------|")
|
|
848
|
+
for parameter in parameters:
|
|
849
|
+
print(
|
|
850
|
+
"| `{}` | `{}` | {} | {} |".format(
|
|
851
|
+
parameter.get("wfKey") or "",
|
|
852
|
+
parameter.get("pythonKeyword") or "",
|
|
853
|
+
"yes" if parameter.get("required") else "no",
|
|
854
|
+
parameter.get("label") or "",
|
|
855
|
+
)
|
|
856
|
+
)
|
|
857
|
+
toolkit_summary = entry.get("toolkitParameterSummary") or {}
|
|
858
|
+
toolkit_parameters = toolkit_summary.get("parameters") if isinstance(toolkit_summary, dict) else None
|
|
859
|
+
if toolkit_parameters:
|
|
860
|
+
print()
|
|
861
|
+
print("| ToolKit key | Name | Type | Cases |")
|
|
862
|
+
print("|-------------|------|------|-------|")
|
|
863
|
+
for parameter in toolkit_parameters:
|
|
864
|
+
print(
|
|
865
|
+
"| `{}` | {} | `{}` | {} |".format(
|
|
866
|
+
parameter.get("key") or "",
|
|
867
|
+
markdown_cell(parameter.get("name") or ""),
|
|
868
|
+
"`, `".join(toolkit_parameter_type_names(parameter)),
|
|
869
|
+
toolkit_parameter_enum_case_preview(parameter),
|
|
870
|
+
)
|
|
871
|
+
)
|
|
872
|
+
trigger_summary = entry.get("toolkitTriggerSummary") or {}
|
|
873
|
+
trigger_parameters = trigger_summary.get("parameters") if isinstance(trigger_summary, dict) else None
|
|
874
|
+
if trigger_parameters:
|
|
875
|
+
print()
|
|
876
|
+
print("| Trigger key | Name | Type | Cases |")
|
|
877
|
+
print("|-------------|------|------|-------|")
|
|
878
|
+
for parameter in trigger_parameters:
|
|
879
|
+
print(
|
|
880
|
+
"| `{}` | {} | `{}` | {} |".format(
|
|
881
|
+
parameter.get("key") or "",
|
|
882
|
+
markdown_cell(parameter.get("name") or ""),
|
|
883
|
+
"`, `".join(toolkit_parameter_type_names(parameter)),
|
|
884
|
+
toolkit_parameter_enum_case_preview(parameter),
|
|
885
|
+
)
|
|
886
|
+
)
|
|
887
|
+
output_types = (
|
|
888
|
+
trigger_summary.get("outputTypeIdentifiers")
|
|
889
|
+
if isinstance(trigger_summary, dict)
|
|
890
|
+
else None
|
|
891
|
+
)
|
|
892
|
+
if output_types:
|
|
893
|
+
print()
|
|
894
|
+
print("- Trigger output types: " + ", ".join(f"`{item}`" for item in output_types))
|
|
895
|
+
workflow_trigger = entry.get("workflowTriggerSample") or {}
|
|
896
|
+
if isinstance(workflow_trigger, dict) and workflow_trigger:
|
|
897
|
+
print()
|
|
898
|
+
observed = workflow_trigger.get("observed")
|
|
899
|
+
if observed is True:
|
|
900
|
+
print("- Workflow trigger sample: observed exported `WFWorkflowTriggers` payload")
|
|
901
|
+
elif observed is False:
|
|
902
|
+
print("- Workflow trigger sample: no exported `WFWorkflowTriggers` sample yet")
|
|
903
|
+
template_status = workflow_trigger.get("templateStatus")
|
|
904
|
+
if template_status:
|
|
905
|
+
print(f"- Template status: `{template_status}`")
|
|
906
|
+
payload = workflow_trigger.get("workflowTrigger") or {}
|
|
907
|
+
if isinstance(payload, dict):
|
|
908
|
+
wf_identifier = payload.get("WFTriggerIdentifier")
|
|
909
|
+
if wf_identifier:
|
|
910
|
+
print(f"- `WFTriggerIdentifier`: `{wf_identifier}`")
|
|
911
|
+
serialized = payload.get("WFTriggerSerializedParameters")
|
|
912
|
+
if isinstance(serialized, dict):
|
|
913
|
+
keys = ", ".join(f"`{key}`" for key in sorted(serialized))
|
|
914
|
+
print(f"- Serialized parameter keys: {keys if keys else 'none'}")
|
|
915
|
+
notes = workflow_trigger.get("notes") or []
|
|
916
|
+
if notes:
|
|
917
|
+
print("- Sample notes: " + " ".join(str(note) for note in notes))
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def markdown_cell(value: str) -> str:
|
|
921
|
+
return str(value).replace("|", "\\|").replace("\n", " ")
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def toolkit_parameter_enum_case_preview(parameter: dict[str, Any], limit: int = 12) -> str:
|
|
925
|
+
enum_types = parameter.get("enumTypes")
|
|
926
|
+
if not isinstance(enum_types, list) or not enum_types:
|
|
927
|
+
return ""
|
|
928
|
+
rendered_types: list[str] = []
|
|
929
|
+
include_type_name = len(enum_types) > 1
|
|
930
|
+
for enum_type in enum_types:
|
|
931
|
+
if not isinstance(enum_type, dict):
|
|
932
|
+
continue
|
|
933
|
+
cases = enum_type.get("cases") or []
|
|
934
|
+
rendered_cases: list[str] = []
|
|
935
|
+
for case in cases[:limit]:
|
|
936
|
+
if not isinstance(case, dict):
|
|
937
|
+
continue
|
|
938
|
+
case_id = markdown_cell(case.get("id") or "")
|
|
939
|
+
title = markdown_cell(case.get("title") or "")
|
|
940
|
+
rendered_cases.append(
|
|
941
|
+
f"`{case_id}`" if not title or title == case_id else f"`{case_id}` ({title})"
|
|
942
|
+
)
|
|
943
|
+
if len(cases) > limit:
|
|
944
|
+
rendered_cases.append(f"... +{len(cases) - limit} more")
|
|
945
|
+
if not rendered_cases:
|
|
946
|
+
continue
|
|
947
|
+
rendered = ", ".join(rendered_cases)
|
|
948
|
+
if include_type_name:
|
|
949
|
+
type_name = markdown_cell(enum_type.get("typePythonName") or "")
|
|
950
|
+
rendered = f"`{type_name}`: {rendered}"
|
|
951
|
+
rendered_types.append(rendered)
|
|
952
|
+
return "<br>".join(rendered_types)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def main() -> int:
|
|
956
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
957
|
+
group = parser.add_mutually_exclusive_group()
|
|
958
|
+
group.add_argument(
|
|
959
|
+
"--identifier",
|
|
960
|
+
help=(
|
|
961
|
+
"WF action/AppIntent identifier, automation trigger identifier, "
|
|
962
|
+
"or short WF action suffix, e.g. additemtolist"
|
|
963
|
+
),
|
|
964
|
+
)
|
|
965
|
+
group.add_argument("--python-name", help="Apple Shortpy function name, e.g. com_apple_shortcuts_add_item_to_list")
|
|
966
|
+
group.add_argument("--query", help="Search names, summaries, keywords, parameters, and Python names")
|
|
967
|
+
parser.add_argument("--target-macos", default=None, help="Target macOS major version, auto, latest, or all")
|
|
968
|
+
parser.add_argument(
|
|
969
|
+
"--target-platform",
|
|
970
|
+
default=None,
|
|
971
|
+
help="Target platform: macos (default), ios/ipados, or all",
|
|
972
|
+
)
|
|
973
|
+
parser.add_argument("--limit", type=int, default=10, help="Maximum search/list results")
|
|
974
|
+
parser.add_argument("--list", action="store_true", help="List catalog entries")
|
|
975
|
+
parser.add_argument("--json", action="store_true", help="Print JSON")
|
|
976
|
+
args = parser.parse_args()
|
|
977
|
+
|
|
978
|
+
try:
|
|
979
|
+
target_macos = resolve_target_macos(args.target_macos)
|
|
980
|
+
target_platform = resolve_target_platform(args.target_platform)
|
|
981
|
+
except ValueError as exc:
|
|
982
|
+
print(str(exc))
|
|
983
|
+
return 2
|
|
984
|
+
catalog = load_catalog()
|
|
985
|
+
parameter_catalog = load_parameter_catalog()
|
|
986
|
+
enum_catalog = load_enum_catalog()
|
|
987
|
+
trigger_catalog = load_trigger_catalog()
|
|
988
|
+
workflow_trigger_catalog = load_workflow_trigger_catalog()
|
|
989
|
+
availability = load_identifier_min_macos()
|
|
990
|
+
|
|
991
|
+
results: list[tuple[str, dict[str, Any]]]
|
|
992
|
+
if args.identifier:
|
|
993
|
+
found = find_by_identifier(catalog, args.identifier)
|
|
994
|
+
if found:
|
|
995
|
+
found = (
|
|
996
|
+
found[0],
|
|
997
|
+
augment_with_parameter_summary(found[0], found[1], parameter_catalog),
|
|
998
|
+
)
|
|
999
|
+
else:
|
|
1000
|
+
found = find_parameter_by_identifier(parameter_catalog, args.identifier)
|
|
1001
|
+
if not found:
|
|
1002
|
+
found = find_trigger_by_identifier(trigger_catalog, args.identifier)
|
|
1003
|
+
results = [found] if found else []
|
|
1004
|
+
elif args.python_name:
|
|
1005
|
+
found = find_by_python_name(catalog, args.python_name)
|
|
1006
|
+
if found:
|
|
1007
|
+
found = (
|
|
1008
|
+
found[0],
|
|
1009
|
+
augment_with_parameter_summary(found[0], found[1], parameter_catalog),
|
|
1010
|
+
)
|
|
1011
|
+
else:
|
|
1012
|
+
found = find_parameter_by_python_name(parameter_catalog, args.python_name)
|
|
1013
|
+
if not found:
|
|
1014
|
+
found = find_trigger_by_python_name(trigger_catalog, args.python_name)
|
|
1015
|
+
results = [found] if found else []
|
|
1016
|
+
elif args.query:
|
|
1017
|
+
results = [
|
|
1018
|
+
(identifier, augment_with_parameter_summary(identifier, entry, parameter_catalog))
|
|
1019
|
+
for identifier, entry in search_entries(catalog, args.query, args.limit)
|
|
1020
|
+
]
|
|
1021
|
+
seen = {identifier for identifier, _ in results}
|
|
1022
|
+
if len(results) < args.limit:
|
|
1023
|
+
for identifier, entry in search_parameter_entries(
|
|
1024
|
+
parameter_catalog,
|
|
1025
|
+
args.query,
|
|
1026
|
+
args.limit - len(results),
|
|
1027
|
+
):
|
|
1028
|
+
if identifier not in seen:
|
|
1029
|
+
results.append((identifier, entry))
|
|
1030
|
+
seen.add(identifier)
|
|
1031
|
+
if len(results) < args.limit:
|
|
1032
|
+
for identifier, entry in search_trigger_entries(
|
|
1033
|
+
trigger_catalog,
|
|
1034
|
+
args.query,
|
|
1035
|
+
args.limit - len(results),
|
|
1036
|
+
):
|
|
1037
|
+
if identifier not in seen:
|
|
1038
|
+
results.append((identifier, entry))
|
|
1039
|
+
seen.add(identifier)
|
|
1040
|
+
else:
|
|
1041
|
+
results = (
|
|
1042
|
+
[
|
|
1043
|
+
(identifier, augment_with_parameter_summary(identifier, entry, parameter_catalog))
|
|
1044
|
+
for identifier, entry in search_entries(catalog, "", args.limit)
|
|
1045
|
+
]
|
|
1046
|
+
if args.list
|
|
1047
|
+
else []
|
|
1048
|
+
)
|
|
1049
|
+
results = [
|
|
1050
|
+
(identifier, filter_entry_for_target_platform(entry, target_platform))
|
|
1051
|
+
for identifier, entry in results
|
|
1052
|
+
]
|
|
1053
|
+
results = [
|
|
1054
|
+
(identifier, augment_with_enum_cases(entry, enum_catalog))
|
|
1055
|
+
for identifier, entry in results
|
|
1056
|
+
]
|
|
1057
|
+
results = [
|
|
1058
|
+
(
|
|
1059
|
+
identifier,
|
|
1060
|
+
augment_with_workflow_trigger_sample(
|
|
1061
|
+
identifier,
|
|
1062
|
+
entry,
|
|
1063
|
+
workflow_trigger_catalog,
|
|
1064
|
+
),
|
|
1065
|
+
)
|
|
1066
|
+
for identifier, entry in results
|
|
1067
|
+
]
|
|
1068
|
+
|
|
1069
|
+
if args.json:
|
|
1070
|
+
result_notes = [
|
|
1071
|
+
target_note(entry_min_macos(identifier, availability), target_macos)
|
|
1072
|
+
for identifier, _ in results
|
|
1073
|
+
]
|
|
1074
|
+
unique_notes = sorted({note for note in result_notes if note})
|
|
1075
|
+
deprecation_notes = [
|
|
1076
|
+
deprecation_note(identifier)
|
|
1077
|
+
for identifier, _ in results
|
|
1078
|
+
]
|
|
1079
|
+
unique_deprecation_notes = sorted({note for note in deprecation_notes if note})
|
|
1080
|
+
platform_notes = [
|
|
1081
|
+
platform_availability_note(entry)
|
|
1082
|
+
for _, entry in results
|
|
1083
|
+
]
|
|
1084
|
+
unique_platform_notes = sorted({note for note in platform_notes if note})
|
|
1085
|
+
target_platform_notes = [
|
|
1086
|
+
target_platform_note(entry, target_platform)
|
|
1087
|
+
for _, entry in results
|
|
1088
|
+
]
|
|
1089
|
+
unique_target_platform_notes = sorted({note for note in target_platform_notes if note})
|
|
1090
|
+
parameter_notes = [
|
|
1091
|
+
parameter_metadata_note(entry, target_macos, entry_min_macos(identifier, availability))
|
|
1092
|
+
for identifier, entry in results
|
|
1093
|
+
]
|
|
1094
|
+
unique_parameter_notes = sorted({note for note in parameter_notes if note})
|
|
1095
|
+
trigger_notes = [
|
|
1096
|
+
trigger_metadata_note(entry, target_macos)
|
|
1097
|
+
for _, entry in results
|
|
1098
|
+
]
|
|
1099
|
+
unique_trigger_notes = sorted({note for note in trigger_notes if note})
|
|
1100
|
+
payload = {
|
|
1101
|
+
"catalog": {
|
|
1102
|
+
"schemaVersion": catalog.get("schemaVersion"),
|
|
1103
|
+
"minimumMacOSMajor": catalog.get("minimumMacOSMajor"),
|
|
1104
|
+
"summary": catalog.get("summary"),
|
|
1105
|
+
"runtimePolicy": catalog.get("runtimePolicy"),
|
|
1106
|
+
},
|
|
1107
|
+
"enumCatalog": {
|
|
1108
|
+
"version": enum_catalog.get("version"),
|
|
1109
|
+
"enumTypeCount": enum_catalog.get("enumTypeCount"),
|
|
1110
|
+
"caseCount": enum_catalog.get("caseCount"),
|
|
1111
|
+
},
|
|
1112
|
+
"workflowTriggerCatalog": {
|
|
1113
|
+
"version": workflow_trigger_catalog.get("version"),
|
|
1114
|
+
"rootKey": workflow_trigger_catalog.get("rootKey"),
|
|
1115
|
+
"observedToolkitTriggerCount": workflow_trigger_catalog.get(
|
|
1116
|
+
"observedToolkitTriggerCount"
|
|
1117
|
+
),
|
|
1118
|
+
"unobservedToolkitTriggerCount": workflow_trigger_catalog.get(
|
|
1119
|
+
"unobservedToolkitTriggerCount"
|
|
1120
|
+
),
|
|
1121
|
+
"minimumMacOSMajor": WORKFLOW_TRIGGER_CATALOG_MIN_MACOS_MAJOR,
|
|
1122
|
+
},
|
|
1123
|
+
"targetMacOSMajor": target_macos,
|
|
1124
|
+
"targetPlatform": target_platform_label(target_platform),
|
|
1125
|
+
"availabilityNote": unique_notes[0] if len(unique_notes) == 1 else None,
|
|
1126
|
+
"deprecationNote": (
|
|
1127
|
+
unique_deprecation_notes[0]
|
|
1128
|
+
if len(unique_deprecation_notes) == 1
|
|
1129
|
+
else None
|
|
1130
|
+
),
|
|
1131
|
+
"parameterMetadataAvailabilityNote": (
|
|
1132
|
+
unique_parameter_notes[0] if len(unique_parameter_notes) == 1 else None
|
|
1133
|
+
),
|
|
1134
|
+
"triggerMetadataAvailabilityNote": (
|
|
1135
|
+
unique_trigger_notes[0] if len(unique_trigger_notes) == 1 else None
|
|
1136
|
+
),
|
|
1137
|
+
"platformAvailabilityNote": (
|
|
1138
|
+
unique_platform_notes[0] if len(unique_platform_notes) == 1 else None
|
|
1139
|
+
),
|
|
1140
|
+
"targetPlatformAvailabilityNote": (
|
|
1141
|
+
unique_target_platform_notes[0]
|
|
1142
|
+
if len(unique_target_platform_notes) == 1
|
|
1143
|
+
else None
|
|
1144
|
+
),
|
|
1145
|
+
"results": [
|
|
1146
|
+
compact_entry(identifier, entry, availability, target_macos, target_platform)
|
|
1147
|
+
for identifier, entry in results
|
|
1148
|
+
],
|
|
1149
|
+
}
|
|
1150
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1151
|
+
return 0 if results else 1
|
|
1152
|
+
|
|
1153
|
+
if not results:
|
|
1154
|
+
print("No Apple-derived grounding entry found.")
|
|
1155
|
+
return 1
|
|
1156
|
+
for index, (identifier, entry) in enumerate(results):
|
|
1157
|
+
if index:
|
|
1158
|
+
print()
|
|
1159
|
+
print_markdown_entry(identifier, entry, availability, target_macos, target_platform)
|
|
1160
|
+
return 0
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
if __name__ == "__main__":
|
|
1164
|
+
raise SystemExit(main())
|