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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortcuts Playground — OpenCode Craig Loop validator plugin
|
|
3
|
+
*
|
|
4
|
+
* OpenCode hook equivalent of Claude Code's PostToolUse hook.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code: hooks/hooks.json + hooks/auto-validate.sh (PostToolUse on Write|Edit)
|
|
7
|
+
* Codex: hooks/hooks.json + hooks/auto-validate.sh (PostToolUse on apply_patch|Write|Edit, with [features].plugin_hooks opt-in)
|
|
8
|
+
* OpenCode: this file — tool.execute.after on write/edit/apply_patch
|
|
9
|
+
*
|
|
10
|
+
* Mapping confirmed against https://opencode.ai/docs/plugins#events
|
|
11
|
+
* Claude PostToolUse → OpenCode tool.execute.after
|
|
12
|
+
* Claude PreToolUse → OpenCode tool.execute.before
|
|
13
|
+
*
|
|
14
|
+
* Behavior:
|
|
15
|
+
* - Fires after every tool that may have touched a Shortcuts plist (write, edit, apply_patch, bash writes)
|
|
16
|
+
* - Filters to .xml/.shortcut extensions + WFWorkflowActions content sniff
|
|
17
|
+
* - Runs the shared validator (skills/shortcuts-playground/scripts/validate_shortcut.py,
|
|
18
|
+
* with fallbacks to claude/skills/... and codex/skills/...)
|
|
19
|
+
* - On failure, injects validator output as additionalContext so the model can iterate (Craig Loop)
|
|
20
|
+
* - Honors SHORTCUTS_PLAYGROUND_TARGET_MACOS / TARGET_PLATFORM via env passthrough (validator reads them)
|
|
21
|
+
*
|
|
22
|
+
* Gap vs Claude Code:
|
|
23
|
+
* - OpenCode plugins cannot exit(2) to block a tool in the same way Claude's PostToolUse hook does.
|
|
24
|
+
* Instead we return additionalContext / throw with validator output so the agent sees the error.
|
|
25
|
+
* The file has still been written; the agent must fix it in the next turn.
|
|
26
|
+
* Documented in opencode/README.md under "Hook parity".
|
|
27
|
+
* - OpenCode's tool.execute.after receives {tool, args, result} — file_path may be under different keys
|
|
28
|
+
* depending on tool. We handle read/write/edit/apply_patch/bash.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { spawnSync } from "node:child_process";
|
|
32
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
33
|
+
import { join, dirname, resolve, isAbsolute } from "node:path";
|
|
34
|
+
|
|
35
|
+
// Resolve plugin root: the opencode/ directory containing this file's package.
|
|
36
|
+
// When installed as .opencode/plugins/validator.ts, dirname is that dir.
|
|
37
|
+
// When running from repo opencode/plugin/, we walk up to repo root.
|
|
38
|
+
function resolvePluginRoot(currentFileDir: string): string {
|
|
39
|
+
// If this file is at <repo>/opencode/plugin/validator.ts, plugin root is <repo>/opencode
|
|
40
|
+
// If installed as <project>/.opencode/plugins/validator.ts, plugin root is <project>/.opencode/plugins
|
|
41
|
+
// Try to find the repo root by walking up and looking for claude/skills/shortcuts-playground
|
|
42
|
+
let dir = currentFileDir;
|
|
43
|
+
for (let i = 0; i < 6; i++) {
|
|
44
|
+
if (existsSync(join(dir, "claude/skills/shortcuts-playground/scripts/validate_shortcut.py"))) return join(dir, "opencode");
|
|
45
|
+
if (existsSync(join(dir, "skills/shortcuts-playground/scripts/validate_shortcut.py"))) return join(dir, "opencode");
|
|
46
|
+
if (existsSync(join(dir, "codex/skills/shortcuts-playground/scripts/validate_shortcut.py"))) return join(dir, "opencode");
|
|
47
|
+
if (existsSync(join(dir, "skills/shortcuts-playground/SKILL.md"))) return dir;
|
|
48
|
+
const parent = dirname(dir);
|
|
49
|
+
if (parent === dir) break;
|
|
50
|
+
dir = parent;
|
|
51
|
+
}
|
|
52
|
+
return currentFileDir;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveSharedValidator(pluginRoot: string, repoRootHint?: string): string | null {
|
|
56
|
+
const candidates: string[] = [];
|
|
57
|
+
|
|
58
|
+
// Prefer shared top-level skills/ if repo has consolidated
|
|
59
|
+
if (repoRootHint) {
|
|
60
|
+
candidates.push(join(repoRootHint, "skills/shortcuts-playground/scripts/validate_shortcut.py"));
|
|
61
|
+
}
|
|
62
|
+
// Repo layout: opencode/ sibling to claude/ and codex/
|
|
63
|
+
const repoRoot = resolve(join(pluginRoot, ".."));
|
|
64
|
+
candidates.push(
|
|
65
|
+
join(repoRoot, "skills/shortcuts-playground/scripts/validate_shortcut.py"),
|
|
66
|
+
join(repoRoot, "claude/skills/shortcuts-playground/scripts/validate_shortcut.py"),
|
|
67
|
+
join(repoRoot, "codex/skills/shortcuts-playground/scripts/validate_shortcut.py"),
|
|
68
|
+
);
|
|
69
|
+
// Also check relative to pluginRoot itself (symlinked install)
|
|
70
|
+
candidates.push(
|
|
71
|
+
join(pluginRoot, "skills/shortcuts-playground/scripts/validate_shortcut.py"),
|
|
72
|
+
join(pluginRoot, "../claude/skills/shortcuts-playground/scripts/validate_shortcut.py"),
|
|
73
|
+
join(pluginRoot, "../codex/skills/shortcuts-playground/scripts/validate_shortcut.py"),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
for (const c of candidates) {
|
|
77
|
+
if (existsSync(c)) return c;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractFilePaths(tool: string, args: Record<string, unknown>, cwd?: string): string[] {
|
|
83
|
+
const paths: string[] = [];
|
|
84
|
+
const add = (p: unknown) => {
|
|
85
|
+
if (typeof p !== "string" || !p) return;
|
|
86
|
+
if (p === "/dev/null") return;
|
|
87
|
+
let abs = p;
|
|
88
|
+
if (!isAbsolute(abs) && cwd) abs = resolve(join(cwd, abs));
|
|
89
|
+
if (!paths.includes(abs)) paths.push(abs);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
add((args as any).file_path);
|
|
93
|
+
add((args as any).path);
|
|
94
|
+
add((args as any).filePath);
|
|
95
|
+
|
|
96
|
+
// apply_patch / bash may embed paths in patch text or command string
|
|
97
|
+
const command = (args as any).command;
|
|
98
|
+
if (typeof command === "string") {
|
|
99
|
+
// Codex-style patch headers: *** Add File: <path>, *** Update File: <path>, *** Move to: <path>
|
|
100
|
+
const re = /^\*\*\* (?:Add|Update) File: (.+)$/gm;
|
|
101
|
+
let m: RegExpExecArray | null;
|
|
102
|
+
while ((m = re.exec(command))) add(m[1].trim());
|
|
103
|
+
const re2 = /^\*\*\* Move to: (.+)$/gm;
|
|
104
|
+
while ((m = re2.exec(command))) add(m[1].trim());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// OpenCode write/edit tools may include file content but path is already captured
|
|
108
|
+
return paths;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const ShortcutsPlaygroundValidator = async ({ project, directory }: { project: unknown; directory: string }) => {
|
|
112
|
+
const pluginDir = dirname(new URL(import.meta.url).pathname);
|
|
113
|
+
const pluginRoot = resolvePluginRoot(pluginDir);
|
|
114
|
+
const validator = resolveSharedValidator(pluginRoot, directory);
|
|
115
|
+
|
|
116
|
+
const py = process.env.SHORTCUTS_PLAYGROUND_PYTHON || "python3";
|
|
117
|
+
|
|
118
|
+
// Early sanity: if python missing or too old, silently no-op (don't spam every write)
|
|
119
|
+
const checkPy = spawnSync(py, ["-c", "import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)"]);
|
|
120
|
+
const pyOk = checkPy.status === 0;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
// PostToolUse-equivalent — runs after each tool completes
|
|
124
|
+
"tool.execute.after": async (input: { tool: string; args: Record<string, unknown> }, output: { result?: unknown }) => {
|
|
125
|
+
if (!validator || !pyOk) return;
|
|
126
|
+
if (!existsSync(validator)) return;
|
|
127
|
+
|
|
128
|
+
// Only care about file-mutating tools
|
|
129
|
+
const tool = input.tool || "";
|
|
130
|
+
const isWriteTool = /^(write|edit|apply_patch|bash)$/i.test(tool) || tool.includes("write") || tool.includes("edit");
|
|
131
|
+
// Also catch generic tool names that may have written a file via bash
|
|
132
|
+
const args = input.args || {};
|
|
133
|
+
|
|
134
|
+
const cwd = directory || process.cwd();
|
|
135
|
+
const filePaths = extractFilePaths(tool, args as Record<string, unknown>, cwd);
|
|
136
|
+
|
|
137
|
+
// Fallback: some tools report output path in result
|
|
138
|
+
if (filePaths.length === 0 && output?.result) {
|
|
139
|
+
const res = output.result as any;
|
|
140
|
+
if (typeof res?.filePath === "string") extractFilePaths(tool, { file_path: res.filePath }, cwd).forEach(p => { if (!filePaths.includes(p)) filePaths.push(p); });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (filePaths.length === 0) return;
|
|
144
|
+
|
|
145
|
+
// Filter + validate each touched file
|
|
146
|
+
const failures: string[] = [];
|
|
147
|
+
for (const fp of filePaths) {
|
|
148
|
+
if (!fp) continue;
|
|
149
|
+
if (!existsSync(fp)) continue;
|
|
150
|
+
if (!fp.endsWith(".xml") && !fp.endsWith(".shortcut")) continue;
|
|
151
|
+
try {
|
|
152
|
+
const content = readFileSync(fp, "utf8");
|
|
153
|
+
if (!content.includes("WFWorkflowActions")) continue;
|
|
154
|
+
} catch {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const result = spawnSync(py, [validator, fp], { encoding: "utf8", timeout: 15000 });
|
|
159
|
+
if (result.status === 0) continue;
|
|
160
|
+
|
|
161
|
+
const out = (result.stdout || "") + (result.stderr || "");
|
|
162
|
+
failures.push(`shortcuts-playground validator failed for ${fp}\n\n${out.trim()}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (failures.length === 0) return;
|
|
166
|
+
|
|
167
|
+
const reason = failures.join("\n\n---\n\n");
|
|
168
|
+
|
|
169
|
+
// In OpenCode, throwing surfaces the message to the agent; returning additionalContext
|
|
170
|
+
// is also supported by some plugin hosts. We throw so the agent sees it even if the
|
|
171
|
+
// host doesn't propagate additionalContext for tool.execute.after.
|
|
172
|
+
// The file has already been written — this is feedback, not a block (gap vs Claude).
|
|
173
|
+
throw new Error(reason);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Default export for opencode `plugin: ["./opencode/plugin/validator.ts"]` style loading
|
|
179
|
+
export default ShortcutsPlaygroundValidator;
|