cc4pm 1.8.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 (108) hide show
  1. package/.claude-plugin/README.md +17 -0
  2. package/.claude-plugin/plugin.json +25 -0
  3. package/LICENSE +21 -0
  4. package/README.md +157 -0
  5. package/README.zh-CN.md +134 -0
  6. package/contexts/dev.md +20 -0
  7. package/contexts/research.md +26 -0
  8. package/contexts/review.md +22 -0
  9. package/examples/CLAUDE.md +100 -0
  10. package/examples/statusline.json +19 -0
  11. package/examples/user-CLAUDE.md +109 -0
  12. package/install.sh +17 -0
  13. package/manifests/install-components.json +173 -0
  14. package/manifests/install-modules.json +335 -0
  15. package/manifests/install-profiles.json +75 -0
  16. package/package.json +117 -0
  17. package/schemas/ecc-install-config.schema.json +58 -0
  18. package/schemas/hooks.schema.json +197 -0
  19. package/schemas/install-components.schema.json +56 -0
  20. package/schemas/install-modules.schema.json +105 -0
  21. package/schemas/install-profiles.schema.json +45 -0
  22. package/schemas/install-state.schema.json +210 -0
  23. package/schemas/package-manager.schema.json +23 -0
  24. package/schemas/plugin.schema.json +58 -0
  25. package/scripts/ci/catalog.js +83 -0
  26. package/scripts/ci/validate-agents.js +81 -0
  27. package/scripts/ci/validate-commands.js +135 -0
  28. package/scripts/ci/validate-hooks.js +239 -0
  29. package/scripts/ci/validate-install-manifests.js +211 -0
  30. package/scripts/ci/validate-no-personal-paths.js +63 -0
  31. package/scripts/ci/validate-rules.js +81 -0
  32. package/scripts/ci/validate-skills.js +54 -0
  33. package/scripts/claw.js +468 -0
  34. package/scripts/doctor.js +110 -0
  35. package/scripts/ecc.js +194 -0
  36. package/scripts/hooks/auto-tmux-dev.js +88 -0
  37. package/scripts/hooks/check-console-log.js +71 -0
  38. package/scripts/hooks/check-hook-enabled.js +12 -0
  39. package/scripts/hooks/cost-tracker.js +78 -0
  40. package/scripts/hooks/doc-file-warning.js +63 -0
  41. package/scripts/hooks/evaluate-session.js +100 -0
  42. package/scripts/hooks/insaits-security-monitor.py +269 -0
  43. package/scripts/hooks/insaits-security-wrapper.js +88 -0
  44. package/scripts/hooks/post-bash-build-complete.js +27 -0
  45. package/scripts/hooks/post-bash-pr-created.js +36 -0
  46. package/scripts/hooks/post-edit-console-warn.js +54 -0
  47. package/scripts/hooks/post-edit-format.js +109 -0
  48. package/scripts/hooks/post-edit-typecheck.js +96 -0
  49. package/scripts/hooks/pre-bash-dev-server-block.js +187 -0
  50. package/scripts/hooks/pre-bash-git-push-reminder.js +28 -0
  51. package/scripts/hooks/pre-bash-tmux-reminder.js +33 -0
  52. package/scripts/hooks/pre-compact.js +48 -0
  53. package/scripts/hooks/pre-write-doc-warn.js +9 -0
  54. package/scripts/hooks/quality-gate.js +168 -0
  55. package/scripts/hooks/run-with-flags-shell.sh +32 -0
  56. package/scripts/hooks/run-with-flags.js +120 -0
  57. package/scripts/hooks/session-end-marker.js +15 -0
  58. package/scripts/hooks/session-end.js +299 -0
  59. package/scripts/hooks/session-start.js +97 -0
  60. package/scripts/hooks/suggest-compact.js +80 -0
  61. package/scripts/install-apply.js +137 -0
  62. package/scripts/install-plan.js +254 -0
  63. package/scripts/lib/hook-flags.js +74 -0
  64. package/scripts/lib/install/apply.js +23 -0
  65. package/scripts/lib/install/config.js +82 -0
  66. package/scripts/lib/install/request.js +113 -0
  67. package/scripts/lib/install/runtime.js +42 -0
  68. package/scripts/lib/install-executor.js +605 -0
  69. package/scripts/lib/install-lifecycle.js +763 -0
  70. package/scripts/lib/install-manifests.js +305 -0
  71. package/scripts/lib/install-state.js +120 -0
  72. package/scripts/lib/install-targets/antigravity-project.js +9 -0
  73. package/scripts/lib/install-targets/claude-home.js +10 -0
  74. package/scripts/lib/install-targets/codex-home.js +10 -0
  75. package/scripts/lib/install-targets/cursor-project.js +10 -0
  76. package/scripts/lib/install-targets/helpers.js +89 -0
  77. package/scripts/lib/install-targets/opencode-home.js +10 -0
  78. package/scripts/lib/install-targets/registry.js +64 -0
  79. package/scripts/lib/orchestration-session.js +299 -0
  80. package/scripts/lib/package-manager.d.ts +119 -0
  81. package/scripts/lib/package-manager.js +431 -0
  82. package/scripts/lib/project-detect.js +428 -0
  83. package/scripts/lib/resolve-formatter.js +185 -0
  84. package/scripts/lib/session-adapters/canonical-session.js +138 -0
  85. package/scripts/lib/session-adapters/claude-history.js +149 -0
  86. package/scripts/lib/session-adapters/dmux-tmux.js +80 -0
  87. package/scripts/lib/session-adapters/registry.js +111 -0
  88. package/scripts/lib/session-aliases.d.ts +136 -0
  89. package/scripts/lib/session-aliases.js +481 -0
  90. package/scripts/lib/session-manager.d.ts +131 -0
  91. package/scripts/lib/session-manager.js +464 -0
  92. package/scripts/lib/shell-split.js +86 -0
  93. package/scripts/lib/skill-improvement/amendify.js +89 -0
  94. package/scripts/lib/skill-improvement/evaluate.js +59 -0
  95. package/scripts/lib/skill-improvement/health.js +118 -0
  96. package/scripts/lib/skill-improvement/observations.js +108 -0
  97. package/scripts/lib/tmux-worktree-orchestrator.js +491 -0
  98. package/scripts/lib/utils.d.ts +183 -0
  99. package/scripts/lib/utils.js +543 -0
  100. package/scripts/list-installed.js +90 -0
  101. package/scripts/orchestrate-codex-worker.sh +92 -0
  102. package/scripts/orchestrate-worktrees.js +108 -0
  103. package/scripts/orchestration-status.js +62 -0
  104. package/scripts/repair.js +97 -0
  105. package/scripts/session-inspect.js +150 -0
  106. package/scripts/setup-package-manager.js +204 -0
  107. package/scripts/skill-create-output.js +244 -0
  108. package/scripts/uninstall.js +96 -0
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Claude Plugin Configuration",
4
+ "type": "object",
5
+ "required": ["name"],
6
+ "properties": {
7
+ "name": { "type": "string" },
8
+ "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
9
+ "description": { "type": "string" },
10
+ "author": {
11
+ "oneOf": [
12
+ { "type": "string" },
13
+ {
14
+ "type": "object",
15
+ "properties": {
16
+ "name": { "type": "string" },
17
+ "url": { "type": "string", "format": "uri" }
18
+ },
19
+ "required": ["name"]
20
+ }
21
+ ]
22
+ },
23
+ "homepage": { "type": "string", "format": "uri" },
24
+ "repository": { "type": "string" },
25
+ "license": { "type": "string" },
26
+ "keywords": {
27
+ "type": "array",
28
+ "items": { "type": "string" }
29
+ },
30
+ "skills": {
31
+ "type": "array",
32
+ "items": { "type": "string" }
33
+ },
34
+ "agents": {
35
+ "type": "array",
36
+ "items": { "type": "string" }
37
+ },
38
+ "features": {
39
+ "type": "object",
40
+ "properties": {
41
+ "agents": { "type": "integer", "minimum": 0 },
42
+ "commands": { "type": "integer", "minimum": 0 },
43
+ "skills": { "type": "integer", "minimum": 0 },
44
+ "configAssets": { "type": "boolean" },
45
+ "hookEvents": {
46
+ "type": "array",
47
+ "items": { "type": "string" }
48
+ },
49
+ "customTools": {
50
+ "type": "array",
51
+ "items": { "type": "string" }
52
+ }
53
+ },
54
+ "additionalProperties": false
55
+ }
56
+ },
57
+ "additionalProperties": false
58
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Catalog agents, commands, and skills from the repo.
4
+ * Outputs JSON with counts and lists for CI/docs sync.
5
+ *
6
+ * Usage: node scripts/ci/catalog.js [--json|--md]
7
+ * Default: --json to stdout
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const ROOT = path.join(__dirname, '../..');
14
+ const AGENTS_DIR = path.join(ROOT, 'agents');
15
+ const COMMANDS_DIR = path.join(ROOT, 'commands');
16
+ const SKILLS_DIR = path.join(ROOT, 'skills');
17
+
18
+ function listAgents() {
19
+ if (!fs.existsSync(AGENTS_DIR)) return [];
20
+ try {
21
+ return fs.readdirSync(AGENTS_DIR)
22
+ .filter(f => f.endsWith('.md'))
23
+ .map(f => f.slice(0, -3))
24
+ .sort();
25
+ } catch (error) {
26
+ throw new Error(`Failed to read agents directory (${AGENTS_DIR}): ${error.message}`);
27
+ }
28
+ }
29
+
30
+ function listCommands() {
31
+ if (!fs.existsSync(COMMANDS_DIR)) return [];
32
+ try {
33
+ return fs.readdirSync(COMMANDS_DIR)
34
+ .filter(f => f.endsWith('.md'))
35
+ .map(f => f.slice(0, -3))
36
+ .sort();
37
+ } catch (error) {
38
+ throw new Error(`Failed to read commands directory (${COMMANDS_DIR}): ${error.message}`);
39
+ }
40
+ }
41
+
42
+ function listSkills() {
43
+ if (!fs.existsSync(SKILLS_DIR)) return [];
44
+ try {
45
+ const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
46
+ return entries
47
+ .filter(e => e.isDirectory() && fs.existsSync(path.join(SKILLS_DIR, e.name, 'SKILL.md')))
48
+ .map(e => e.name)
49
+ .sort();
50
+ } catch (error) {
51
+ throw new Error(`Failed to read skills directory (${SKILLS_DIR}): ${error.message}`);
52
+ }
53
+ }
54
+
55
+ function run() {
56
+ const agents = listAgents();
57
+ const commands = listCommands();
58
+ const skills = listSkills();
59
+
60
+ const catalog = {
61
+ agents: { count: agents.length, list: agents },
62
+ commands: { count: commands.length, list: commands },
63
+ skills: { count: skills.length, list: skills }
64
+ };
65
+
66
+ const format = process.argv[2] === '--md' ? 'md' : 'json';
67
+ if (format === 'md') {
68
+ console.log('# cc4pm Catalog (generated)\n');
69
+ console.log(`- **Agents:** ${catalog.agents.count}`);
70
+ console.log(`- **Commands:** ${catalog.commands.count}`);
71
+ console.log(`- **Skills:** ${catalog.skills.count}\n`);
72
+ console.log('## Agents\n');
73
+ catalog.agents.list.forEach(a => { console.log(`- ${a}`); });
74
+ console.log('\n## Commands\n');
75
+ catalog.commands.list.forEach(c => { console.log(`- ${c}`); });
76
+ console.log('\n## Skills\n');
77
+ catalog.skills.list.forEach(s => { console.log(`- ${s}`); });
78
+ } else {
79
+ console.log(JSON.stringify(catalog, null, 2));
80
+ }
81
+ }
82
+
83
+ run();
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validate agent markdown files have required frontmatter
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const AGENTS_DIR = path.join(__dirname, '../../agents');
10
+ const REQUIRED_FIELDS = ['model', 'tools'];
11
+ const VALID_MODELS = ['haiku', 'sonnet', 'opus'];
12
+
13
+ function extractFrontmatter(content) {
14
+ // Strip BOM if present (UTF-8 BOM: \uFEFF)
15
+ const cleanContent = content.replace(/^\uFEFF/, '');
16
+ // Support both LF and CRLF line endings
17
+ const match = cleanContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
18
+ if (!match) return null;
19
+
20
+ const frontmatter = {};
21
+ const lines = match[1].split(/\r?\n/);
22
+ for (const line of lines) {
23
+ const colonIdx = line.indexOf(':');
24
+ if (colonIdx > 0) {
25
+ const key = line.slice(0, colonIdx).trim();
26
+ const value = line.slice(colonIdx + 1).trim();
27
+ frontmatter[key] = value;
28
+ }
29
+ }
30
+ return frontmatter;
31
+ }
32
+
33
+ function validateAgents() {
34
+ if (!fs.existsSync(AGENTS_DIR)) {
35
+ console.log('No agents directory found, skipping validation');
36
+ process.exit(0);
37
+ }
38
+
39
+ const files = fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md'));
40
+ let hasErrors = false;
41
+
42
+ for (const file of files) {
43
+ const filePath = path.join(AGENTS_DIR, file);
44
+ let content;
45
+ try {
46
+ content = fs.readFileSync(filePath, 'utf-8');
47
+ } catch (err) {
48
+ console.error(`ERROR: ${file} - ${err.message}`);
49
+ hasErrors = true;
50
+ continue;
51
+ }
52
+ const frontmatter = extractFrontmatter(content);
53
+
54
+ if (!frontmatter) {
55
+ console.error(`ERROR: ${file} - Missing frontmatter`);
56
+ hasErrors = true;
57
+ continue;
58
+ }
59
+
60
+ for (const field of REQUIRED_FIELDS) {
61
+ if (!frontmatter[field] || (typeof frontmatter[field] === 'string' && !frontmatter[field].trim())) {
62
+ console.error(`ERROR: ${file} - Missing required field: ${field}`);
63
+ hasErrors = true;
64
+ }
65
+ }
66
+
67
+ // Validate model is a known value
68
+ if (frontmatter.model && !VALID_MODELS.includes(frontmatter.model)) {
69
+ console.error(`ERROR: ${file} - Invalid model '${frontmatter.model}'. Must be one of: ${VALID_MODELS.join(', ')}`);
70
+ hasErrors = true;
71
+ }
72
+ }
73
+
74
+ if (hasErrors) {
75
+ process.exit(1);
76
+ }
77
+
78
+ console.log(`Validated ${files.length} agent files`);
79
+ }
80
+
81
+ validateAgents();
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validate command markdown files are non-empty, readable,
4
+ * and have valid cross-references to other commands, agents, and skills.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const ROOT_DIR = path.join(__dirname, '../..');
11
+ const COMMANDS_DIR = path.join(ROOT_DIR, 'commands');
12
+ const AGENTS_DIR = path.join(ROOT_DIR, 'agents');
13
+ const SKILLS_DIR = path.join(ROOT_DIR, 'skills');
14
+
15
+ function validateCommands() {
16
+ if (!fs.existsSync(COMMANDS_DIR)) {
17
+ console.log('No commands directory found, skipping validation');
18
+ process.exit(0);
19
+ }
20
+
21
+ const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md'));
22
+ let hasErrors = false;
23
+ let warnCount = 0;
24
+
25
+ // Build set of valid command names (without .md extension)
26
+ const validCommands = new Set(files.map(f => f.replace(/\.md$/, '')));
27
+
28
+ // Build set of valid agent names (without .md extension)
29
+ const validAgents = new Set();
30
+ if (fs.existsSync(AGENTS_DIR)) {
31
+ for (const f of fs.readdirSync(AGENTS_DIR)) {
32
+ if (f.endsWith('.md')) {
33
+ validAgents.add(f.replace(/\.md$/, ''));
34
+ }
35
+ }
36
+ }
37
+
38
+ // Build set of valid skill directory names
39
+ const validSkills = new Set();
40
+ if (fs.existsSync(SKILLS_DIR)) {
41
+ for (const f of fs.readdirSync(SKILLS_DIR)) {
42
+ const skillPath = path.join(SKILLS_DIR, f);
43
+ try {
44
+ if (fs.statSync(skillPath).isDirectory()) {
45
+ validSkills.add(f);
46
+ }
47
+ } catch {
48
+ // skip unreadable entries
49
+ }
50
+ }
51
+ }
52
+
53
+ for (const file of files) {
54
+ const filePath = path.join(COMMANDS_DIR, file);
55
+ let content;
56
+ try {
57
+ content = fs.readFileSync(filePath, 'utf-8');
58
+ } catch (err) {
59
+ console.error(`ERROR: ${file} - ${err.message}`);
60
+ hasErrors = true;
61
+ continue;
62
+ }
63
+
64
+ // Validate the file is non-empty readable markdown
65
+ if (content.trim().length === 0) {
66
+ console.error(`ERROR: ${file} - Empty command file`);
67
+ hasErrors = true;
68
+ continue;
69
+ }
70
+
71
+ // Strip fenced code blocks before checking cross-references.
72
+ // Examples/templates inside ``` blocks are not real references.
73
+ const contentNoCodeBlocks = content.replace(/```[\s\S]*?```/g, '');
74
+
75
+ // Check cross-references to other commands (e.g., `/build-fix`)
76
+ // Skip lines that describe hypothetical output (e.g., "→ Creates: `/new-table`")
77
+ // Process line-by-line so ALL command refs per line are captured
78
+ // (previous anchored regex /^.*`\/...`.*$/gm only matched the last ref per line)
79
+ for (const line of contentNoCodeBlocks.split('\n')) {
80
+ if (/creates:|would create:/i.test(line)) continue;
81
+ const lineRefs = line.matchAll(/`\/([a-z][-a-z0-9]*)`/g);
82
+ for (const match of lineRefs) {
83
+ const refName = match[1];
84
+ if (!validCommands.has(refName)) {
85
+ console.error(`ERROR: ${file} - references non-existent command /${refName}`);
86
+ hasErrors = true;
87
+ }
88
+ }
89
+ }
90
+
91
+ // Check agent references (e.g., "agents/planner.md" or "`planner` agent")
92
+ const agentPathRefs = contentNoCodeBlocks.matchAll(/agents\/([a-z][-a-z0-9]*)\.md/g);
93
+ for (const match of agentPathRefs) {
94
+ const refName = match[1];
95
+ if (!validAgents.has(refName)) {
96
+ console.error(`ERROR: ${file} - references non-existent agent agents/${refName}.md`);
97
+ hasErrors = true;
98
+ }
99
+ }
100
+
101
+ // Check skill directory references (e.g., "skills/tdd-workflow/")
102
+ const skillRefs = contentNoCodeBlocks.matchAll(/skills\/([a-z][-a-z0-9]*)\//g);
103
+ for (const match of skillRefs) {
104
+ const refName = match[1];
105
+ if (!validSkills.has(refName)) {
106
+ console.warn(`WARN: ${file} - references skill directory skills/${refName}/ (not found locally)`);
107
+ warnCount++;
108
+ }
109
+ }
110
+
111
+ // Check agent name references in workflow diagrams (e.g., "planner -> tdd-guide")
112
+ const workflowLines = contentNoCodeBlocks.matchAll(/^([a-z][-a-z0-9]*(?:\s*->\s*[a-z][-a-z0-9]*)+)$/gm);
113
+ for (const match of workflowLines) {
114
+ const agents = match[1].split(/\s*->\s*/);
115
+ for (const agent of agents) {
116
+ if (!validAgents.has(agent)) {
117
+ console.error(`ERROR: ${file} - workflow references non-existent agent "${agent}"`);
118
+ hasErrors = true;
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ if (hasErrors) {
125
+ process.exit(1);
126
+ }
127
+
128
+ let msg = `Validated ${files.length} command files`;
129
+ if (warnCount > 0) {
130
+ msg += ` (${warnCount} warnings)`;
131
+ }
132
+ console.log(msg);
133
+ }
134
+
135
+ validateCommands();
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validate hooks.json schema and hook entry rules.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const vm = require('vm');
9
+ const Ajv = require('ajv');
10
+
11
+ const HOOKS_FILE = path.join(__dirname, '../../hooks/hooks.json');
12
+ const HOOKS_SCHEMA_PATH = path.join(__dirname, '../../schemas/hooks.schema.json');
13
+ const VALID_EVENTS = [
14
+ 'SessionStart',
15
+ 'UserPromptSubmit',
16
+ 'PreToolUse',
17
+ 'PermissionRequest',
18
+ 'PostToolUse',
19
+ 'PostToolUseFailure',
20
+ 'Notification',
21
+ 'SubagentStart',
22
+ 'Stop',
23
+ 'SubagentStop',
24
+ 'PreCompact',
25
+ 'InstructionsLoaded',
26
+ 'TeammateIdle',
27
+ 'TaskCompleted',
28
+ 'ConfigChange',
29
+ 'WorktreeCreate',
30
+ 'WorktreeRemove',
31
+ 'SessionEnd',
32
+ ];
33
+ const VALID_HOOK_TYPES = ['command', 'http', 'prompt', 'agent'];
34
+ const EVENTS_WITHOUT_MATCHER = new Set(['UserPromptSubmit', 'Notification', 'Stop', 'SubagentStop']);
35
+
36
+ function isNonEmptyString(value) {
37
+ return typeof value === 'string' && value.trim().length > 0;
38
+ }
39
+
40
+ function isNonEmptyStringArray(value) {
41
+ return Array.isArray(value) && value.length > 0 && value.every(item => isNonEmptyString(item));
42
+ }
43
+
44
+ /**
45
+ * Validate a single hook entry has required fields and valid inline JS
46
+ * @param {object} hook - Hook object with type and command fields
47
+ * @param {string} label - Label for error messages (e.g., "PreToolUse[0].hooks[1]")
48
+ * @returns {boolean} true if errors were found
49
+ */
50
+ function validateHookEntry(hook, label) {
51
+ let hasErrors = false;
52
+
53
+ if (!hook.type || typeof hook.type !== 'string') {
54
+ console.error(`ERROR: ${label} missing or invalid 'type' field`);
55
+ hasErrors = true;
56
+ } else if (!VALID_HOOK_TYPES.includes(hook.type)) {
57
+ console.error(`ERROR: ${label} has unsupported hook type '${hook.type}'`);
58
+ hasErrors = true;
59
+ }
60
+
61
+ if ('timeout' in hook && (typeof hook.timeout !== 'number' || hook.timeout < 0)) {
62
+ console.error(`ERROR: ${label} 'timeout' must be a non-negative number`);
63
+ hasErrors = true;
64
+ }
65
+
66
+ if (hook.type === 'command') {
67
+ if ('async' in hook && typeof hook.async !== 'boolean') {
68
+ console.error(`ERROR: ${label} 'async' must be a boolean`);
69
+ hasErrors = true;
70
+ }
71
+
72
+ if (!isNonEmptyString(hook.command) && !isNonEmptyStringArray(hook.command)) {
73
+ console.error(`ERROR: ${label} missing or invalid 'command' field`);
74
+ hasErrors = true;
75
+ } else if (typeof hook.command === 'string') {
76
+ const nodeEMatch = hook.command.match(/^node -e "(.*)"$/s);
77
+ if (nodeEMatch) {
78
+ try {
79
+ new vm.Script(nodeEMatch[1].replace(/\\\\/g, '\\').replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\t/g, '\t'));
80
+ } catch (syntaxErr) {
81
+ console.error(`ERROR: ${label} has invalid inline JS: ${syntaxErr.message}`);
82
+ hasErrors = true;
83
+ }
84
+ }
85
+ }
86
+
87
+ return hasErrors;
88
+ }
89
+
90
+ if ('async' in hook) {
91
+ console.error(`ERROR: ${label} 'async' is only supported for command hooks`);
92
+ hasErrors = true;
93
+ }
94
+
95
+ if (hook.type === 'http') {
96
+ if (!isNonEmptyString(hook.url)) {
97
+ console.error(`ERROR: ${label} missing or invalid 'url' field`);
98
+ hasErrors = true;
99
+ }
100
+
101
+ if ('headers' in hook && (typeof hook.headers !== 'object' || hook.headers === null || Array.isArray(hook.headers) || !Object.values(hook.headers).every(value => typeof value === 'string'))) {
102
+ console.error(`ERROR: ${label} 'headers' must be an object with string values`);
103
+ hasErrors = true;
104
+ }
105
+
106
+ if ('allowedEnvVars' in hook && (!Array.isArray(hook.allowedEnvVars) || !hook.allowedEnvVars.every(value => isNonEmptyString(value)))) {
107
+ console.error(`ERROR: ${label} 'allowedEnvVars' must be an array of strings`);
108
+ hasErrors = true;
109
+ }
110
+
111
+ return hasErrors;
112
+ }
113
+
114
+ if (!isNonEmptyString(hook.prompt)) {
115
+ console.error(`ERROR: ${label} missing or invalid 'prompt' field`);
116
+ hasErrors = true;
117
+ }
118
+
119
+ if ('model' in hook && !isNonEmptyString(hook.model)) {
120
+ console.error(`ERROR: ${label} 'model' must be a non-empty string`);
121
+ hasErrors = true;
122
+ }
123
+
124
+ return hasErrors;
125
+ }
126
+
127
+ function validateHooks() {
128
+ if (!fs.existsSync(HOOKS_FILE)) {
129
+ console.log('No hooks.json found, skipping validation');
130
+ process.exit(0);
131
+ }
132
+
133
+ let data;
134
+ try {
135
+ data = JSON.parse(fs.readFileSync(HOOKS_FILE, 'utf-8'));
136
+ } catch (e) {
137
+ console.error(`ERROR: Invalid JSON in hooks.json: ${e.message}`);
138
+ process.exit(1);
139
+ }
140
+
141
+ // Validate against JSON schema
142
+ if (fs.existsSync(HOOKS_SCHEMA_PATH)) {
143
+ const schema = JSON.parse(fs.readFileSync(HOOKS_SCHEMA_PATH, 'utf-8'));
144
+ const ajv = new Ajv({ allErrors: true });
145
+ const validate = ajv.compile(schema);
146
+ const valid = validate(data);
147
+ if (!valid) {
148
+ for (const err of validate.errors) {
149
+ console.error(`ERROR: hooks.json schema: ${err.instancePath || '/'} ${err.message}`);
150
+ }
151
+ process.exit(1);
152
+ }
153
+ }
154
+
155
+ // Support both object format { hooks: {...} } and array format
156
+ const hooks = data.hooks || data;
157
+ let hasErrors = false;
158
+ let totalMatchers = 0;
159
+
160
+ if (typeof hooks === 'object' && !Array.isArray(hooks)) {
161
+ // Object format: { EventType: [matchers] }
162
+ for (const [eventType, matchers] of Object.entries(hooks)) {
163
+ if (!VALID_EVENTS.includes(eventType)) {
164
+ console.error(`ERROR: Invalid event type: ${eventType}`);
165
+ hasErrors = true;
166
+ continue;
167
+ }
168
+
169
+ if (!Array.isArray(matchers)) {
170
+ console.error(`ERROR: ${eventType} must be an array`);
171
+ hasErrors = true;
172
+ continue;
173
+ }
174
+
175
+ for (let i = 0; i < matchers.length; i++) {
176
+ const matcher = matchers[i];
177
+ if (typeof matcher !== 'object' || matcher === null) {
178
+ console.error(`ERROR: ${eventType}[${i}] is not an object`);
179
+ hasErrors = true;
180
+ continue;
181
+ }
182
+ if (!('matcher' in matcher) && !EVENTS_WITHOUT_MATCHER.has(eventType)) {
183
+ console.error(`ERROR: ${eventType}[${i}] missing 'matcher' field`);
184
+ hasErrors = true;
185
+ } else if ('matcher' in matcher && typeof matcher.matcher !== 'string' && (typeof matcher.matcher !== 'object' || matcher.matcher === null)) {
186
+ console.error(`ERROR: ${eventType}[${i}] has invalid 'matcher' field`);
187
+ hasErrors = true;
188
+ }
189
+ if (!matcher.hooks || !Array.isArray(matcher.hooks)) {
190
+ console.error(`ERROR: ${eventType}[${i}] missing 'hooks' array`);
191
+ hasErrors = true;
192
+ } else {
193
+ // Validate each hook entry
194
+ for (let j = 0; j < matcher.hooks.length; j++) {
195
+ if (validateHookEntry(matcher.hooks[j], `${eventType}[${i}].hooks[${j}]`)) {
196
+ hasErrors = true;
197
+ }
198
+ }
199
+ }
200
+ totalMatchers++;
201
+ }
202
+ }
203
+ } else if (Array.isArray(hooks)) {
204
+ // Array format (legacy)
205
+ for (let i = 0; i < hooks.length; i++) {
206
+ const hook = hooks[i];
207
+ if (!('matcher' in hook)) {
208
+ console.error(`ERROR: Hook ${i} missing 'matcher' field`);
209
+ hasErrors = true;
210
+ } else if (typeof hook.matcher !== 'string' && (typeof hook.matcher !== 'object' || hook.matcher === null)) {
211
+ console.error(`ERROR: Hook ${i} has invalid 'matcher' field`);
212
+ hasErrors = true;
213
+ }
214
+ if (!hook.hooks || !Array.isArray(hook.hooks)) {
215
+ console.error(`ERROR: Hook ${i} missing 'hooks' array`);
216
+ hasErrors = true;
217
+ } else {
218
+ // Validate each hook entry
219
+ for (let j = 0; j < hook.hooks.length; j++) {
220
+ if (validateHookEntry(hook.hooks[j], `Hook ${i}.hooks[${j}]`)) {
221
+ hasErrors = true;
222
+ }
223
+ }
224
+ }
225
+ totalMatchers++;
226
+ }
227
+ } else {
228
+ console.error('ERROR: hooks.json must be an object or array');
229
+ process.exit(1);
230
+ }
231
+
232
+ if (hasErrors) {
233
+ process.exit(1);
234
+ }
235
+
236
+ console.log(`Validated ${totalMatchers} hook matchers`);
237
+ }
238
+
239
+ validateHooks();