scc-universal 1.1.0 → 1.2.0

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 (39) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.cursor/hooks.json +105 -64
  3. package/.cursor/skills/configure-scc/SKILL.md +20 -20
  4. package/.cursor/skills/mcp-server-patterns/SKILL.md +1 -1
  5. package/.cursor/skills/sf-harness-audit/SKILL.md +6 -6
  6. package/.cursor/skills/sf-quickstart/SKILL.md +7 -7
  7. package/.cursor-plugin/plugin.json +2 -2
  8. package/README.md +51 -37
  9. package/docs/ARCHITECTURE.md +4 -4
  10. package/docs/authoring-guide.md +2 -2
  11. package/docs/workflow-examples.md +38 -38
  12. package/hooks/hooks.json +56 -71
  13. package/manifests/install-modules.json +5 -3
  14. package/package.json +4 -3
  15. package/schemas/hooks.schema.json +83 -72
  16. package/schemas/plugin.schema.json +59 -21
  17. package/scripts/cli/install-apply.js +9 -9
  18. package/scripts/hooks/doc-file-warning.js +3 -1
  19. package/scripts/hooks/governor-check.js +3 -2
  20. package/scripts/hooks/post-bash-build-complete.js +3 -2
  21. package/scripts/hooks/post-bash-pr-created.js +4 -2
  22. package/scripts/hooks/post-edit-console-warn.js +3 -1
  23. package/scripts/hooks/post-edit-format.js +3 -2
  24. package/scripts/hooks/post-edit-typecheck.js +3 -2
  25. package/scripts/hooks/post-write.js +3 -1
  26. package/scripts/hooks/pre-bash-git-push-reminder.js +3 -2
  27. package/scripts/hooks/pre-bash-tmux-reminder.js +3 -1
  28. package/scripts/hooks/pre-tool-use.js +3 -1
  29. package/scripts/hooks/quality-gate.js +3 -2
  30. package/scripts/hooks/sfdx-scanner-check.js +3 -1
  31. package/scripts/hooks/sfdx-validate.js +3 -1
  32. package/scripts/lib/hook-input.js +105 -0
  33. package/scripts/lib/hooks-adapter.js +265 -0
  34. package/scripts/lib/install-executor.js +153 -1
  35. package/scripts/scc.js +14 -14
  36. package/skills/configure-scc/SKILL.md +20 -20
  37. package/skills/mcp-server-patterns/SKILL.md +1 -1
  38. package/skills/sf-harness-audit/SKILL.md +6 -6
  39. package/skills/sf-quickstart/SKILL.md +7 -7
@@ -5,119 +5,130 @@
5
5
  "description": "JSON Schema for SCC hooks.json — defines Claude Code lifecycle hooks for Salesforce development",
6
6
  "type": "object",
7
7
  "required": ["hooks"],
8
- "additionalProperties": false,
9
8
  "properties": {
10
- "$schema": {
9
+ "description": {
11
10
  "type": "string",
12
- "description": "Schema reference URI"
11
+ "description": "Top-level description of this hooks configuration (plugin hooks only)"
13
12
  },
14
13
  "hooks": {
15
14
  "type": "object",
16
15
  "description": "Map of lifecycle event names to arrays of hook group definitions",
17
- "additionalProperties": false,
18
16
  "properties": {
19
- "SessionStart": {
20
- "$ref": "#/definitions/hookGroupArray",
21
- "description": "Hooks fired when a Claude Code session begins"
22
- },
23
- "PreToolUse": {
24
- "$ref": "#/definitions/hookGroupArray",
25
- "description": "Hooks fired before a tool is used"
26
- },
27
- "PostToolUse": {
28
- "$ref": "#/definitions/hookGroupArray",
29
- "description": "Hooks fired after a tool completes"
30
- },
31
- "PostToolUseFailure": {
32
- "$ref": "#/definitions/hookGroupArray",
33
- "description": "Hooks fired when a tool use fails"
34
- },
35
- "PreCompact": {
36
- "$ref": "#/definitions/hookGroupArray",
37
- "description": "Hooks fired before context compaction"
38
- },
39
- "Stop": {
40
- "$ref": "#/definitions/hookGroupArray",
41
- "description": "Hooks fired when Claude Code agent stops"
42
- },
43
- "SessionEnd": {
44
- "$ref": "#/definitions/hookGroupArray",
45
- "description": "Hooks fired when a session ends"
46
- }
47
- }
17
+ "SessionStart": { "$ref": "#/definitions/hookGroupArray" },
18
+ "UserPromptSubmit": { "$ref": "#/definitions/hookGroupArray" },
19
+ "PreToolUse": { "$ref": "#/definitions/hookGroupArray" },
20
+ "PermissionRequest": { "$ref": "#/definitions/hookGroupArray" },
21
+ "PermissionDenied": { "$ref": "#/definitions/hookGroupArray" },
22
+ "PostToolUse": { "$ref": "#/definitions/hookGroupArray" },
23
+ "PostToolUseFailure": { "$ref": "#/definitions/hookGroupArray" },
24
+ "Notification": { "$ref": "#/definitions/hookGroupArray" },
25
+ "SubagentStart": { "$ref": "#/definitions/hookGroupArray" },
26
+ "SubagentStop": { "$ref": "#/definitions/hookGroupArray" },
27
+ "TaskCreated": { "$ref": "#/definitions/hookGroupArray" },
28
+ "TaskCompleted": { "$ref": "#/definitions/hookGroupArray" },
29
+ "Stop": { "$ref": "#/definitions/hookGroupArray" },
30
+ "StopFailure": { "$ref": "#/definitions/hookGroupArray" },
31
+ "TeammateIdle": { "$ref": "#/definitions/hookGroupArray" },
32
+ "InstructionsLoaded": { "$ref": "#/definitions/hookGroupArray" },
33
+ "ConfigChange": { "$ref": "#/definitions/hookGroupArray" },
34
+ "CwdChanged": { "$ref": "#/definitions/hookGroupArray" },
35
+ "FileChanged": { "$ref": "#/definitions/hookGroupArray" },
36
+ "WorktreeCreate": { "$ref": "#/definitions/hookGroupArray" },
37
+ "WorktreeRemove": { "$ref": "#/definitions/hookGroupArray" },
38
+ "PreCompact": { "$ref": "#/definitions/hookGroupArray" },
39
+ "PostCompact": { "$ref": "#/definitions/hookGroupArray" },
40
+ "Elicitation": { "$ref": "#/definitions/hookGroupArray" },
41
+ "ElicitationResult": { "$ref": "#/definitions/hookGroupArray" },
42
+ "SessionEnd": { "$ref": "#/definitions/hookGroupArray" }
43
+ },
44
+ "additionalProperties": false
48
45
  }
49
46
  },
50
47
  "definitions": {
51
48
  "hookGroupArray": {
52
49
  "type": "array",
53
- "items": {
54
- "$ref": "#/definitions/hookGroup"
55
- },
50
+ "items": { "$ref": "#/definitions/hookGroup" },
56
51
  "minItems": 1
57
52
  },
58
53
  "hookGroup": {
59
54
  "type": "object",
60
55
  "required": ["hooks"],
61
- "additionalProperties": false,
62
56
  "properties": {
63
57
  "matcher": {
64
58
  "type": "string",
65
- "description": "Optional tool name matcher (e.g., 'Bash', 'Write', 'Read')"
59
+ "description": "Regex pattern to filter when hooks fire. Use tool name for tool events, source for SessionStart, etc."
66
60
  },
67
61
  "hooks": {
68
62
  "type": "array",
69
- "description": "Array of individual hook definitions in this group",
70
- "items": {
71
- "$ref": "#/definitions/hookDefinition"
72
- },
63
+ "items": { "$ref": "#/definitions/hookDefinition" },
73
64
  "minItems": 1
74
- },
75
- "description": {
76
- "type": "string",
77
- "description": "Human-readable description of this hook group"
78
- },
79
- "profile": {
80
- "type": "string",
81
- "enum": ["minimal", "standard", "strict"],
82
- "description": "Minimum profile level required for this hook group to run"
83
65
  }
84
- }
66
+ },
67
+ "additionalProperties": false
85
68
  },
86
69
  "hookDefinition": {
87
70
  "type": "object",
88
- "required": ["type", "command"],
89
- "additionalProperties": false,
71
+ "required": ["type"],
90
72
  "properties": {
91
73
  "type": {
92
74
  "type": "string",
93
- "enum": ["command"],
94
- "description": "Hook execution type — currently only 'command' is supported"
75
+ "enum": ["command", "http", "prompt", "agent"],
76
+ "description": "Hook execution type"
95
77
  },
96
78
  "command": {
97
79
  "type": "string",
98
- "description": "Shell command to execute. Use ${CLAUDE_PLUGIN_ROOT} for the plugin root path.",
99
- "minLength": 1
80
+ "description": "Shell command to execute (type: command)"
100
81
  },
101
- "async": {
102
- "type": "boolean",
103
- "description": "Whether to run this hook asynchronously (non-blocking)",
104
- "default": false
82
+ "url": {
83
+ "type": "string",
84
+ "description": "URL to POST to (type: http)"
85
+ },
86
+ "prompt": {
87
+ "type": "string",
88
+ "description": "Prompt text for LLM evaluation (type: prompt or agent)"
89
+ },
90
+ "model": {
91
+ "type": "string",
92
+ "description": "Model to use for prompt/agent hooks"
93
+ },
94
+ "if": {
95
+ "type": "string",
96
+ "description": "Permission rule syntax filter, e.g. Bash(git *) or Edit(*.cls)"
105
97
  },
106
98
  "timeout": {
107
99
  "type": "integer",
108
- "description": "Timeout in seconds before the hook is killed",
109
- "minimum": 1,
110
- "maximum": 120,
111
- "default": 30
100
+ "description": "Timeout in seconds",
101
+ "minimum": 1
112
102
  },
113
- "env": {
103
+ "async": {
104
+ "type": "boolean",
105
+ "description": "Run in background without blocking (command hooks only)"
106
+ },
107
+ "shell": {
108
+ "type": "string",
109
+ "enum": ["bash", "powershell"],
110
+ "description": "Shell to use (command hooks only)"
111
+ },
112
+ "statusMessage": {
113
+ "type": "string",
114
+ "description": "Custom spinner message while hook runs"
115
+ },
116
+ "once": {
117
+ "type": "boolean",
118
+ "description": "Run only once per session (skills/agents only)"
119
+ },
120
+ "headers": {
114
121
  "type": "object",
115
- "description": "Additional environment variables to set for this hook",
116
- "additionalProperties": {
117
- "type": "string"
118
- }
122
+ "description": "HTTP headers (type: http)",
123
+ "additionalProperties": { "type": "string" }
124
+ },
125
+ "allowedEnvVars": {
126
+ "type": "array",
127
+ "description": "Env vars allowed in header interpolation (type: http)",
128
+ "items": { "type": "string" }
119
129
  }
120
- }
130
+ },
131
+ "additionalProperties": false
121
132
  }
122
133
  }
123
134
  }
@@ -2,10 +2,9 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://scc-universal/schemas/plugin.schema.json",
4
4
  "title": "SCC Plugin Manifest",
5
- "description": "JSON Schema for .claude-plugin/plugin.json — describes the SCC plugin for the Claude Code marketplace",
5
+ "description": "JSON Schema for plugin.json — describes the SCC plugin for Claude Code and Cursor marketplaces",
6
6
  "type": "object",
7
- "required": ["name", "version", "displayName", "description", "license", "engines", "main"],
8
- "additionalProperties": false,
7
+ "required": ["name", "version", "description"],
9
8
  "properties": {
10
9
  "$schema": {
11
10
  "type": "string",
@@ -35,13 +34,27 @@
35
34
  "maxLength": 300
36
35
  },
37
36
  "author": {
38
- "type": "string",
39
- "description": "Plugin author name or organization"
37
+ "oneOf": [
38
+ {
39
+ "type": "string",
40
+ "description": "Plugin author name or organization"
41
+ },
42
+ {
43
+ "type": "object",
44
+ "description": "Plugin author details",
45
+ "required": ["name"],
46
+ "properties": {
47
+ "name": { "type": "string" },
48
+ "email": { "type": "string", "format": "email" },
49
+ "url": { "type": "string", "format": "uri" }
50
+ },
51
+ "additionalProperties": false
52
+ }
53
+ ]
40
54
  },
41
55
  "license": {
42
56
  "type": "string",
43
- "description": "SPDX license identifier",
44
- "enum": ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "GPL-3.0", "LGPL-3.0", "MPL-2.0"]
57
+ "description": "SPDX license identifier"
45
58
  },
46
59
  "keywords": {
47
60
  "type": "array",
@@ -53,15 +66,17 @@
53
66
  "minItems": 1,
54
67
  "maxItems": 20
55
68
  },
69
+ "logo": {
70
+ "type": "string",
71
+ "description": "Relative path to the plugin logo image (SVG or PNG)"
72
+ },
56
73
  "engines": {
57
74
  "type": "object",
58
75
  "description": "Required harness versions",
59
- "additionalProperties": false,
60
76
  "properties": {
61
77
  "claude-code": {
62
78
  "type": "string",
63
- "description": "Required Claude Code version (semver range)",
64
- "pattern": "^[>=^~]?\\d+\\.\\d+\\.\\d+.*$"
79
+ "description": "Required Claude Code version (semver range)"
65
80
  },
66
81
  "node": {
67
82
  "type": "string",
@@ -71,17 +86,37 @@
71
86
  },
72
87
  "main": {
73
88
  "type": "string",
74
- "description": "Relative path to the primary plugin entry file (hooks.json)",
89
+ "description": "Relative path to the primary plugin entry file",
75
90
  "minLength": 1
76
91
  },
77
92
  "agents": {
78
- "type": "string",
79
- "description": "Relative path to agents directory"
93
+ "oneOf": [
94
+ {
95
+ "type": "string",
96
+ "description": "Relative path to agents directory"
97
+ },
98
+ {
99
+ "type": "array",
100
+ "description": "Array of relative paths to individual agent files",
101
+ "items": {
102
+ "type": "string",
103
+ "minLength": 1
104
+ }
105
+ }
106
+ ]
80
107
  },
81
108
  "skills": {
82
109
  "type": "string",
83
110
  "description": "Relative path to skills directory"
84
111
  },
112
+ "hooks": {
113
+ "type": "string",
114
+ "description": "Relative path to hooks configuration file"
115
+ },
116
+ "mcpServers": {
117
+ "type": "string",
118
+ "description": "Relative path to MCP servers configuration file"
119
+ },
85
120
  "commands": {
86
121
  "type": "string",
87
122
  "description": "Relative path to commands directory"
@@ -91,17 +126,20 @@
91
126
  "description": "Relative path to rules directory"
92
127
  },
93
128
  "repository": {
94
- "type": "object",
95
- "description": "Source repository information",
96
- "properties": {
97
- "type": {
129
+ "oneOf": [
130
+ {
98
131
  "type": "string",
99
- "enum": ["git", "svn"]
132
+ "description": "Repository URL"
100
133
  },
101
- "url": {
102
- "type": "string"
134
+ {
135
+ "type": "object",
136
+ "description": "Source repository information",
137
+ "properties": {
138
+ "type": { "type": "string", "enum": ["git", "svn"] },
139
+ "url": { "type": "string" }
140
+ }
103
141
  }
104
- }
142
+ ]
105
143
  },
106
144
  "homepage": {
107
145
  "type": "string",
@@ -17,11 +17,11 @@ const { loadInstallConfig } = require('../lib/install-config');
17
17
 
18
18
  function showHelp(exitCode = 0) {
19
19
  console.log(`
20
- scc install — Install SCC content
20
+ scc-universal install — Install SCC content
21
21
 
22
22
  Usage:
23
- scc install [target] [options]
24
- scc install --profile <name> --target <name>
23
+ scc-universal install [target] [options]
24
+ scc-universal install --profile <name> --target <name>
25
25
 
26
26
  Shorthand targets:
27
27
  apex Install Apex profile content
@@ -37,12 +37,12 @@ Options:
37
37
  --help, -h Show this help
38
38
 
39
39
  Examples:
40
- scc install apex
41
- scc install all
42
- scc install --config scc-install.json
43
- scc install --config scc-install.json --target cursor
44
- scc install --profile security --target claude
45
- scc install --profile lwc --target cursor --dry-run
40
+ scc-universal install apex
41
+ scc-universal install all
42
+ scc-universal install --config scc-install.json
43
+ scc-universal install --config scc-install.json --target cursor
44
+ scc-universal install --profile security --target claude
45
+ scc-universal install --profile lwc --target cursor --dry-run
46
46
  `);
47
47
  process.exit(exitCode);
48
48
  }
@@ -48,7 +48,9 @@ process.stdin.on('data', c => {
48
48
  process.stdin.on('end', () => {
49
49
  try {
50
50
  const input = JSON.parse(data);
51
- const filePath = String((input.tool_input && input.tool_input.file_path) || '');
51
+ const { normalizeInput } = require('../lib/hook-input');
52
+ const ctx = normalizeInput(input);
53
+ const filePath = ctx.filePath;
52
54
 
53
55
  if (filePath && !isAllowedDocPath(filePath)) {
54
56
  console.error('[Hook] WARNING: Non-standard documentation file detected');
@@ -195,8 +195,9 @@ function checkGovernorLimits(filePath) {
195
195
  function run(rawInput) {
196
196
  try {
197
197
  const input = JSON.parse(rawInput);
198
- const filePath = String(input.tool_input?.file_path || '');
199
- checkGovernorLimits(filePath);
198
+ const { normalizeInput } = require('../lib/hook-input');
199
+ const ctx = normalizeInput(input);
200
+ checkGovernorLimits(ctx.filePath);
200
201
  } catch {
201
202
  // Ignore errors
202
203
  }
@@ -22,8 +22,9 @@ process.stdin.on('data', chunk => {
22
22
  process.stdin.on('end', () => {
23
23
  try {
24
24
  const input = JSON.parse(raw);
25
- const cmd = String(input.tool_input?.command || '');
26
- if (/(sf\s+project\s+deploy|sf\s+deploy|sfdx\s+force:source:deploy|npm run build|pnpm build|yarn build)/.test(cmd)) {
25
+ const { normalizeInput } = require('../lib/hook-input');
26
+ const ctx = normalizeInput(input);
27
+ if (/(sf\s+project\s+deploy|sf\s+deploy|sfdx\s+force:source:deploy|npm run build|pnpm build|yarn build)/.test(ctx.command)) {
27
28
  console.error('[Hook] Build/deploy completed - review results above');
28
29
  }
29
30
  } catch {
@@ -22,10 +22,12 @@ process.stdin.on('data', chunk => {
22
22
  process.stdin.on('end', () => {
23
23
  try {
24
24
  const input = JSON.parse(raw);
25
- const cmd = String(input.tool_input?.command || '');
25
+ const { normalizeInput } = require('../lib/hook-input');
26
+ const ctx = normalizeInput(input);
27
+ const cmd = ctx.command;
26
28
 
27
29
  if (/\bgh\s+pr\s+create\b/.test(cmd)) {
28
- const out = String(input.tool_output?.output || '');
30
+ const out = ctx.output;
29
31
  const match = out.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/);
30
32
  if (match) {
31
33
  const prUrl = match[0];
@@ -25,7 +25,9 @@ process.stdin.on('data', chunk => {
25
25
  process.stdin.on('end', () => {
26
26
  try {
27
27
  const input = JSON.parse(data);
28
- const filePath = input.tool_input?.file_path;
28
+ const { normalizeInput } = require('../lib/hook-input');
29
+ const ctx = normalizeInput(input);
30
+ const filePath = ctx.filePath;
29
31
 
30
32
  if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
31
33
  let content;
@@ -54,8 +54,9 @@ function tryFormat(filePath) {
54
54
  function run(rawInput) {
55
55
  try {
56
56
  const input = JSON.parse(rawInput);
57
- const filePath = String(input.tool_input?.file_path || '');
58
- tryFormat(filePath);
57
+ const { normalizeInput } = require('../lib/hook-input');
58
+ const ctx = normalizeInput(input);
59
+ tryFormat(ctx.filePath);
59
60
  } catch {
60
61
  // Ignore errors
61
62
  }
@@ -73,8 +73,9 @@ function checkFile(filePath) {
73
73
  function run(rawInput) {
74
74
  try {
75
75
  const input = JSON.parse(rawInput);
76
- const filePath = String(input.tool_input?.file_path || '');
77
- checkFile(filePath);
76
+ const { normalizeInput } = require('../lib/hook-input');
77
+ const ctx = normalizeInput(input);
78
+ checkFile(ctx.filePath);
78
79
  } catch {
79
80
  // Ignore parse errors
80
81
  }
@@ -153,7 +153,9 @@ rl.on('close', () => {
153
153
  process.exit(0);
154
154
  }
155
155
 
156
- const filePath = (input.tool_input && input.tool_input.file_path) || '';
156
+ const { normalizeInput } = require('../lib/hook-input');
157
+ const ctx = normalizeInput(input);
158
+ const filePath = ctx.filePath;
157
159
  if (!filePath) process.exit(0);
158
160
 
159
161
  const fileType = classifyFile(filePath);
@@ -22,8 +22,9 @@ process.stdin.on('data', chunk => {
22
22
  process.stdin.on('end', () => {
23
23
  try {
24
24
  const input = JSON.parse(raw);
25
- const cmd = String(input.tool_input?.command || '');
26
- if (/\bgit\s+push\b/.test(cmd)) {
25
+ const { normalizeInput } = require('../lib/hook-input');
26
+ const ctx = normalizeInput(input);
27
+ if (/\bgit\s+push\b/.test(ctx.command)) {
27
28
  console.error('[Hook] Review changes before push...');
28
29
  console.error('[Hook] Continuing with push (remove this hook to add interactive review)');
29
30
  }
@@ -28,7 +28,9 @@ process.stdin.on('data', chunk => {
28
28
  process.stdin.on('end', () => {
29
29
  try {
30
30
  const input = JSON.parse(raw);
31
- const cmd = String(input.tool_input?.command || '');
31
+ const { normalizeInput } = require('../lib/hook-input');
32
+ const ctx = normalizeInput(input);
33
+ const cmd = ctx.command;
32
34
 
33
35
  if (
34
36
  process.platform !== 'win32' &&
@@ -105,7 +105,9 @@ function processInput(input) {
105
105
  return null;
106
106
  }
107
107
 
108
- const command = (input.tool_input && input.tool_input.command) || '';
108
+ const { normalizeInput } = require('../lib/hook-input');
109
+ const ctx = normalizeInput(input);
110
+ const command = ctx.command;
109
111
  if (!command) return null;
110
112
 
111
113
  // Skip if command doesn't involve SF/SFDX
@@ -226,8 +226,9 @@ function maybeRunQualityGate(filePath) {
226
226
  function run(rawInput) {
227
227
  try {
228
228
  const input = JSON.parse(rawInput);
229
- const filePath = String(input.tool_input?.file_path || '');
230
- maybeRunQualityGate(filePath);
229
+ const { normalizeInput } = require('../lib/hook-input');
230
+ const ctx = normalizeInput(input);
231
+ maybeRunQualityGate(ctx.filePath);
231
232
  } catch {
232
233
  // Ignore parse errors
233
234
  }
@@ -84,7 +84,9 @@ function runScanner(files) {
84
84
  function run(rawInput) {
85
85
  try {
86
86
  const input = JSON.parse(rawInput);
87
- const command = String(input.tool_input?.command || '');
87
+ const { normalizeInput } = require('../lib/hook-input');
88
+ const ctx = normalizeInput(input);
89
+ const command = ctx.command;
88
90
 
89
91
  // Only intercept git push and sf deploy commands
90
92
  const isGitPush = /\bgit\s+push\b/.test(command);
@@ -100,7 +100,9 @@ rl.on('close', () => {
100
100
  process.exit(0);
101
101
  }
102
102
 
103
- const command = (input.tool_input && input.tool_input.command) || '';
103
+ const { normalizeInput } = require('../lib/hook-input');
104
+ const ctx = normalizeInput(input);
105
+ const command = ctx.command;
104
106
  const warnings = validateCommand(command);
105
107
 
106
108
  if (warnings.length === 0) {