@hailer/mcp 1.0.28 → 1.1.2
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/.claude/.session-checked +1 -0
- package/.claude/agents/agent-ada-skill-builder.md +10 -2
- package/.claude/agents/agent-alejandro-function-fields.md +104 -37
- package/.claude/agents/agent-bjorn-config-audit.md +41 -21
- package/.claude/agents/agent-builder-agent-creator.md +13 -3
- package/.claude/agents/agent-code-simplifier.md +53 -0
- package/.claude/agents/agent-dmitri-activity-crud.md +126 -11
- package/.claude/agents/agent-giuseppe-app-builder.md +212 -22
- package/.claude/agents/agent-gunther-mcp-tools.md +7 -36
- package/.claude/agents/agent-helga-workflow-config.md +75 -10
- package/.claude/agents/agent-igor-activity-mover-automation.md +125 -0
- package/.claude/agents/agent-ingrid-doc-templates.md +164 -36
- package/.claude/agents/agent-ivan-monolith.md +154 -0
- package/.claude/agents/agent-kenji-data-reader.md +15 -8
- package/.claude/agents/agent-lars-code-inspector.md +56 -8
- package/.claude/agents/agent-marco-mockup-builder.md +110 -0
- package/.claude/agents/agent-marcus-api-documenter.md +323 -0
- package/.claude/agents/agent-marketplace-publisher.md +232 -72
- package/.claude/agents/agent-marketplace-reviewer.md +255 -79
- package/.claude/agents/agent-permissions-handler.md +208 -0
- package/.claude/agents/agent-simple-writer.md +48 -0
- package/.claude/agents/agent-svetlana-code-review.md +127 -14
- package/.claude/agents/agent-tanya-test-runner.md +333 -0
- package/.claude/agents/agent-ui-designer.md +100 -0
- package/.claude/agents/agent-viktor-sql-insights.md +19 -6
- package/.claude/agents/agent-web-search.md +55 -0
- package/.claude/agents/agent-yevgeni-discussions.md +7 -1
- package/.claude/agents/agent-zara-zapier.md +159 -0
- package/.claude/commands/app-squad.md +135 -0
- package/.claude/commands/audit-squad.md +158 -0
- package/.claude/commands/autoplan.md +563 -0
- package/.claude/commands/cleanup-squad.md +98 -0
- package/.claude/commands/config-squad.md +106 -0
- package/.claude/commands/crud-squad.md +87 -0
- package/.claude/commands/data-squad.md +97 -0
- package/.claude/commands/debug-squad.md +303 -0
- package/.claude/commands/doc-squad.md +65 -0
- package/.claude/commands/handoff.md +137 -0
- package/.claude/commands/health.md +49 -0
- package/.claude/commands/help.md +2 -1
- package/.claude/commands/help:agents.md +96 -16
- package/.claude/commands/help:commands.md +55 -11
- package/.claude/commands/help:faq.md +16 -1
- package/.claude/commands/help:skills.md +93 -0
- package/.claude/commands/hotfix-squad.md +112 -0
- package/.claude/commands/integration-squad.md +82 -0
- package/.claude/commands/janitor-squad.md +167 -0
- package/.claude/commands/learn-auto.md +120 -0
- package/.claude/commands/learn.md +120 -0
- package/.claude/commands/mcp-list.md +27 -0
- package/.claude/commands/onboard-squad.md +140 -0
- package/.claude/commands/plan-workspace.md +732 -0
- package/.claude/commands/prd.md +131 -0
- package/.claude/commands/project-status.md +82 -0
- package/.claude/commands/publish.md +138 -0
- package/.claude/commands/recap.md +69 -0
- package/.claude/commands/restore.md +64 -0
- package/.claude/commands/review-squad.md +152 -0
- package/.claude/commands/save.md +24 -0
- package/.claude/commands/stats.md +19 -0
- package/.claude/commands/swarm.md +210 -0
- package/.claude/commands/tool-builder.md +3 -1
- package/.claude/commands/ws-pull.md +1 -1
- package/.claude/commands/yolo-off.md +17 -0
- package/.claude/commands/yolo.md +82 -0
- package/.claude/hooks/_shared-memory.cjs +305 -0
- package/.claude/hooks/_utils.cjs +134 -0
- package/.claude/hooks/agent-failure-detector.cjs +164 -79
- package/.claude/hooks/agent-usage-logger.cjs +204 -0
- package/.claude/hooks/app-edit-guard.cjs +20 -4
- package/.claude/hooks/auto-learn.cjs +316 -0
- package/.claude/hooks/bash-guard.cjs +282 -0
- package/.claude/hooks/builder-mode-manager.cjs +183 -54
- package/.claude/hooks/bulk-activity-guard.cjs +283 -0
- package/.claude/hooks/context-watchdog.cjs +292 -0
- package/.claude/hooks/delegation-reminder.cjs +478 -0
- package/.claude/hooks/design-system-lint.cjs +283 -0
- package/.claude/hooks/post-scaffold-hook.cjs +16 -3
- package/.claude/hooks/prompt-guard.cjs +366 -0
- package/.claude/hooks/publish-template-guard.cjs +16 -0
- package/.claude/hooks/session-start.cjs +35 -0
- package/.claude/hooks/shared-memory-writer.cjs +147 -0
- package/.claude/hooks/skill-injector.cjs +140 -0
- package/.claude/hooks/skill-usage-logger.cjs +258 -0
- package/.claude/hooks/src-edit-guard.cjs +16 -1
- package/.claude/hooks/sync-marketplace-agents.cjs +53 -8
- package/.claude/scripts/yolo-toggle.cjs +142 -0
- package/.claude/settings.json +141 -14
- package/.claude/skills/SDK-activity-patterns/SKILL.md +428 -0
- package/.claude/skills/SDK-document-templates/SKILL.md +1033 -0
- package/.claude/skills/SDK-function-fields/SKILL.md +542 -0
- package/.claude/skills/SDK-generate-skill/SKILL.md +92 -0
- package/.claude/skills/SDK-init-skill/SKILL.md +127 -0
- package/.claude/skills/SDK-insight-queries/SKILL.md +787 -0
- package/.claude/skills/SDK-ws-config-skill/SKILL.md +1139 -0
- package/.claude/skills/agent-structure/SKILL.md +98 -0
- package/.claude/skills/api-documentation-patterns/SKILL.md +474 -0
- package/.claude/skills/chrome-mcp-reference/SKILL.md +370 -0
- package/.claude/skills/delegation-routing/SKILL.md +202 -0
- package/.claude/skills/frontend-design/SKILL.md +254 -0
- package/.claude/skills/hailer-activity-mover/SKILL.md +213 -0
- package/.claude/skills/hailer-api-client/SKILL.md +518 -0
- package/.claude/skills/hailer-app-builder/SKILL.md +939 -11
- package/.claude/skills/hailer-apps-pictures/SKILL.md +269 -0
- package/.claude/skills/hailer-design-system/SKILL.md +235 -0
- package/.claude/skills/hailer-monolith-automations/SKILL.md +686 -0
- package/.claude/skills/hailer-permissions-system/SKILL.md +121 -0
- package/.claude/skills/hailer-project-protocol/SKILL.md +488 -0
- package/.claude/skills/hailer-rest-api/SKILL.md +61 -0
- package/.claude/skills/hailer-rest-api/hailer-activities.md +184 -0
- package/.claude/skills/hailer-rest-api/hailer-admin.md +473 -0
- package/.claude/skills/hailer-rest-api/hailer-calendar.md +256 -0
- package/.claude/skills/hailer-rest-api/hailer-feed.md +249 -0
- package/.claude/skills/hailer-rest-api/hailer-insights.md +195 -0
- package/.claude/skills/hailer-rest-api/hailer-messaging.md +276 -0
- package/.claude/skills/hailer-rest-api/hailer-workflows.md +283 -0
- package/.claude/skills/insight-join-patterns/SKILL.md +3 -0
- package/.claude/skills/integration-patterns/SKILL.md +421 -0
- package/.claude/skills/json-only-output/SKILL.md +52 -12
- package/.claude/skills/lsp-setup/SKILL.md +160 -0
- package/.claude/skills/mcp-direct-tools/SKILL.md +153 -0
- package/.claude/skills/optional-parameters/SKILL.md +32 -23
- package/.claude/skills/publish-hailer-app/SKILL.md +76 -12
- package/.claude/skills/testing-patterns/SKILL.md +630 -0
- package/.claude/skills/tool-builder/SKILL.md +250 -0
- package/.claude/skills/tool-parameter-usage/SKILL.md +59 -45
- package/.claude/skills/tool-response-verification/SKILL.md +82 -48
- package/.claude/skills/zapier-hailer-patterns/SKILL.md +581 -0
- package/.env.example +26 -7
- package/CLAUDE.md +290 -224
- package/dist/CLAUDE.md +370 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.js +101 -101
- package/dist/bot/bot-config.d.ts +26 -0
- package/dist/bot/bot-config.js +135 -0
- package/dist/bot/bot-manager.d.ts +40 -0
- package/dist/bot/bot-manager.js +137 -0
- package/dist/bot/bot.d.ts +127 -0
- package/dist/bot/bot.js +1328 -0
- package/dist/bot/operation-logger.d.ts +28 -0
- package/dist/bot/operation-logger.js +132 -0
- package/dist/bot/services/conversation-manager.d.ts +60 -0
- package/dist/bot/services/conversation-manager.js +246 -0
- package/dist/bot/services/index.d.ts +9 -0
- package/dist/bot/services/index.js +18 -0
- package/dist/bot/services/message-classifier.d.ts +42 -0
- package/dist/bot/services/message-classifier.js +228 -0
- package/dist/bot/services/message-formatter.d.ts +88 -0
- package/dist/bot/services/message-formatter.js +411 -0
- package/dist/bot/services/session-logger.d.ts +162 -0
- package/dist/bot/services/session-logger.js +724 -0
- package/dist/bot/services/token-billing.d.ts +78 -0
- package/dist/bot/services/token-billing.js +233 -0
- package/dist/bot/services/types.d.ts +169 -0
- package/dist/bot/services/types.js +12 -0
- package/dist/bot/services/typing-indicator.d.ts +23 -0
- package/dist/bot/services/typing-indicator.js +60 -0
- package/dist/bot/services/workspace-schema-cache.d.ts +122 -0
- package/dist/bot/services/workspace-schema-cache.js +506 -0
- package/dist/bot/tool-executor.d.ts +28 -0
- package/dist/bot/tool-executor.js +48 -0
- package/dist/bot/workspace-overview.d.ts +12 -0
- package/dist/bot/workspace-overview.js +94 -0
- package/dist/cli.d.ts +1 -8
- package/dist/cli.js +1 -249
- package/dist/config.d.ts +96 -3
- package/dist/config.js +148 -37
- package/dist/core.d.ts +5 -0
- package/dist/core.js +61 -8
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/lib/logger.d.ts +0 -1
- package/dist/lib/logger.js +39 -23
- package/dist/lib/request-logger.d.ts +77 -0
- package/dist/lib/request-logger.js +147 -0
- package/dist/mcp/UserContextCache.js +16 -13
- package/dist/mcp/hailer-clients.js +18 -17
- package/dist/mcp/signal-handler.js +29 -13
- package/dist/mcp/tool-registry.d.ts +4 -15
- package/dist/mcp/tool-registry.js +94 -32
- package/dist/mcp/tools/activity.js +28 -69
- package/dist/mcp/tools/app-core.js +9 -4
- package/dist/mcp/tools/app-marketplace.js +22 -12
- package/dist/mcp/tools/app-member.js +5 -2
- package/dist/mcp/tools/app-scaffold.js +32 -18
- package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
- package/dist/mcp/tools/bot-config/constants.js +94 -0
- package/dist/mcp/tools/bot-config/core.d.ts +253 -0
- package/dist/mcp/tools/bot-config/core.js +2456 -0
- package/dist/mcp/tools/bot-config/index.d.ts +10 -0
- package/dist/mcp/tools/bot-config/index.js +59 -0
- package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
- package/dist/mcp/tools/bot-config/tools.js +15 -0
- package/dist/mcp/tools/bot-config/types.d.ts +50 -0
- package/dist/mcp/tools/bot-config/types.js +6 -0
- package/dist/mcp/tools/discussion.js +107 -77
- package/dist/mcp/tools/document.d.ts +11 -0
- package/dist/mcp/tools/document.js +741 -0
- package/dist/mcp/tools/file.js +5 -2
- package/dist/mcp/tools/insight.js +36 -12
- package/dist/mcp/tools/investigate.d.ts +9 -0
- package/dist/mcp/tools/investigate.js +254 -0
- package/dist/mcp/tools/user.d.ts +2 -4
- package/dist/mcp/tools/user.js +9 -50
- package/dist/mcp/tools/workflow.d.ts +1 -0
- package/dist/mcp/tools/workflow.js +164 -52
- package/dist/mcp/utils/hailer-api-client.js +26 -17
- package/dist/mcp/webhook-handler.d.ts +64 -3
- package/dist/mcp/webhook-handler.js +219 -9
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.js +237 -25
- package/dist/plugins/bug-fixer/index.d.ts +2 -0
- package/dist/plugins/bug-fixer/index.js +18 -0
- package/dist/plugins/bug-fixer/tools.d.ts +45 -0
- package/dist/plugins/bug-fixer/tools.js +1096 -0
- package/package.json +10 -10
- package/scripts/test-hal-tools.ts +154 -0
- package/.claude/agents/agent-nora-name-functions.md +0 -123
- package/.claude/assistant-knowledge.md +0 -23
- package/.claude/commands/install-plugin.md +0 -261
- package/.claude/commands/list-plugins.md +0 -42
- package/.claude/commands/marketplace-setup.md +0 -33
- package/.claude/commands/publish-plugin.md +0 -55
- package/.claude/commands/uninstall-plugin.md +0 -87
- package/.claude/hooks/interactive-mode.cjs +0 -87
- package/.claude/hooks/mcp-server-guard.cjs +0 -108
- package/.claude/skills/marketplace-publishing.md +0 -155
- package/dist/bot/chat-bot.d.ts +0 -31
- package/dist/bot/chat-bot.js +0 -357
- package/dist/mcp/tools/metrics.d.ts +0 -13
- package/dist/mcp/tools/metrics.js +0 -546
- package/dist/stdio-server.d.ts +0 -14
- package/dist/stdio-server.js +0 -114
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* <hook-name>design-system-lint</hook-name>
|
|
4
|
+
*
|
|
5
|
+
* <purpose>
|
|
6
|
+
* Warns if app code doesn't follow Hailer Design System patterns.
|
|
7
|
+
* Runs after Giuseppe writes/edits files in app directories.
|
|
8
|
+
* </purpose>
|
|
9
|
+
*
|
|
10
|
+
* <triggers>
|
|
11
|
+
* - PostToolUse on Write and Edit
|
|
12
|
+
* - Only for files in apps/ or containing Chakra imports
|
|
13
|
+
* </triggers>
|
|
14
|
+
*
|
|
15
|
+
* <checks>
|
|
16
|
+
* - Hardcoded colors (hex, rgb, hsl)
|
|
17
|
+
* - External icon libraries (react-icons, heroicons, etc.)
|
|
18
|
+
* - Plain HTML table elements instead of Chakra Table
|
|
19
|
+
* - Missing theme import when using ChakraProvider
|
|
20
|
+
* </checks>
|
|
21
|
+
*
|
|
22
|
+
* <behavior>
|
|
23
|
+
* 1. Check if file is in app directory or has Chakra imports
|
|
24
|
+
* 2. Scan content for violations
|
|
25
|
+
* 3. Output warnings to stderr (non-blocking)
|
|
26
|
+
* </behavior>
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
// Skip in yolo mode
|
|
33
|
+
try {
|
|
34
|
+
const statePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', 'yolo-state.json');
|
|
35
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
36
|
+
if (state.mode === 'yolo') process.exit(0);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// ENOENT is expected when not in yolo mode - only warn on unexpected errors
|
|
39
|
+
if (e.code !== 'ENOENT' && !e.message.includes('Unexpected')) {
|
|
40
|
+
console.error(`[design-system-lint] Warning: ${e.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Read hook input from stdin
|
|
45
|
+
let input = '';
|
|
46
|
+
process.stdin.setEncoding('utf8');
|
|
47
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
48
|
+
process.stdin.on('end', () => {
|
|
49
|
+
try {
|
|
50
|
+
const data = JSON.parse(input);
|
|
51
|
+
processHook(data);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(`[design-system-lint] Warning: ${e.message}`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Design system violation patterns
|
|
59
|
+
const VIOLATIONS = [
|
|
60
|
+
{
|
|
61
|
+
name: 'hardcoded-hex-color',
|
|
62
|
+
pattern: /(?:color|bg|background|border|fill|stroke)[:=]\s*(?:["'`]#[0-9a-fA-F]{3,8}["'`]|\$\{[^}]*#[0-9a-fA-F]{3,8}[^}]*\})/g,
|
|
63
|
+
message: 'Hardcoded hex color found',
|
|
64
|
+
suggestion: 'Use Chakra color tokens (e.g., "gray.500", "green.400") or semantic tokens ("subtleText", "bodyText")'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'hardcoded-rgb-color',
|
|
68
|
+
pattern: /(?:color|bg|background|border)[:=]\s*(?:["'`]rgba?\([^)]+\)["'`]|\$\{[^}]*rgba?\([^)]+\)[^}]*\})/g,
|
|
69
|
+
message: 'Hardcoded RGB/RGBA color found',
|
|
70
|
+
suggestion: 'Use Chakra color tokens instead'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'external-icons',
|
|
74
|
+
pattern: /import\s+.*\s+from\s+["'](react-icons|@heroicons|@fortawesome|lucide-react|@tabler\/icons)/g,
|
|
75
|
+
message: 'External icon library imported',
|
|
76
|
+
suggestion: 'Use Hailer icons from design-system/theme/icons (e.g., HailerPlus, HailerSettings)'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'plain-html-table',
|
|
80
|
+
pattern: /<(table|thead|tbody|tr|th|td)[\s>]/gi,
|
|
81
|
+
message: 'Plain HTML table elements found',
|
|
82
|
+
suggestion: 'Use Chakra UI Table components: <Table>, <Thead>, <Tbody>, <Tr>, <Th>, <Td>'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'missing-theme-import',
|
|
86
|
+
pattern: /ChakraProvider/,
|
|
87
|
+
message: 'ChakraProvider without theme prop',
|
|
88
|
+
suggestion: 'Import and pass the Hailer theme: <ChakraProvider theme={theme}>'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'inline-style',
|
|
92
|
+
pattern: /style\s*=\s*\{\s*\{[^}]*(?:color|background|border)[^}]*\}\s*\}/g,
|
|
93
|
+
message: 'Inline style with color/background',
|
|
94
|
+
suggestion: 'Use Chakra style props instead of inline styles'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'wrong-stat-pattern',
|
|
98
|
+
pattern: /<Box[^>]*>[\s\S]*?<Text[^>]*fontSize=["']sm["'][^>]*>[\s\S]*?<\/Text>[\s\S]*?<(?:Heading|Text)[^>]*fontSize=["'](?:2xl|3xl|xl)["']/g,
|
|
99
|
+
message: 'Manual stat card pattern detected',
|
|
100
|
+
suggestion: 'Use Chakra Stat components: <Stat>, <StatLabel>, <StatNumber>, <StatHelpText>'
|
|
101
|
+
}
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Files/patterns to skip
|
|
105
|
+
const SKIP_PATTERNS = [
|
|
106
|
+
/node_modules/,
|
|
107
|
+
/\.d\.ts$/,
|
|
108
|
+
/\.json$/,
|
|
109
|
+
/\.md$/,
|
|
110
|
+
/\.css$/,
|
|
111
|
+
/theme\.ts$/, // Don't lint the theme file itself
|
|
112
|
+
/customColors\.ts$/
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
function shouldLintFile(filePath) {
|
|
116
|
+
// Skip certain files
|
|
117
|
+
for (const pattern of SKIP_PATTERNS) {
|
|
118
|
+
if (pattern.test(filePath)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if it's in apps/ directory
|
|
124
|
+
if (filePath.includes('/apps/') || filePath.includes('\\apps\\')) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if it's a TypeScript/JavaScript file that might be an app
|
|
129
|
+
if (/\.(tsx?|jsx?)$/.test(filePath)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function lintContent(content, filePath) {
|
|
137
|
+
const warnings = [];
|
|
138
|
+
|
|
139
|
+
// Only lint if file has Chakra imports (indicates it's a UI file)
|
|
140
|
+
const hasChakraImport = content.includes('@chakra-ui/react') || content.includes('chakra-ui');
|
|
141
|
+
const isInApps = filePath.includes('/apps/') || filePath.includes('\\apps\\');
|
|
142
|
+
|
|
143
|
+
if (!hasChakraImport && !isInApps) {
|
|
144
|
+
return warnings;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Single-pass scan: loop through violations once, check content for each pattern
|
|
148
|
+
// Performance: O(n*m) where n=violations, m=pattern matches. Acceptable for typical files (<50KB)
|
|
149
|
+
for (const violation of VIOLATIONS) {
|
|
150
|
+
const matches = content.match(violation.pattern);
|
|
151
|
+
if (matches) {
|
|
152
|
+
// Special handling for missing-theme-import
|
|
153
|
+
if (violation.name === 'missing-theme-import') {
|
|
154
|
+
// Only warn if ChakraProvider exists but no theme= prop nearby
|
|
155
|
+
if (content.includes('ChakraProvider') && !content.includes('theme={') && !content.includes('theme=')) {
|
|
156
|
+
warnings.push({
|
|
157
|
+
name: violation.name,
|
|
158
|
+
message: violation.message,
|
|
159
|
+
suggestion: violation.suggestion,
|
|
160
|
+
count: 1
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
warnings.push({
|
|
167
|
+
name: violation.name,
|
|
168
|
+
message: violation.message,
|
|
169
|
+
suggestion: violation.suggestion,
|
|
170
|
+
count: matches.length,
|
|
171
|
+
examples: matches.slice(0, 2).map(m => m.substring(0, 60))
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return warnings;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function processHook(data) {
|
|
180
|
+
const { tool_name, tool_input, tool_response } = data;
|
|
181
|
+
|
|
182
|
+
// Only check Write and Edit tools on PostToolUse
|
|
183
|
+
if (tool_name !== 'Write' && tool_name !== 'Edit') {
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Need tool_response to confirm it's PostToolUse
|
|
188
|
+
if (tool_response === undefined) {
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const filePath = tool_input?.file_path;
|
|
193
|
+
if (!filePath) {
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if we should lint this file
|
|
198
|
+
if (!shouldLintFile(filePath)) {
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Read the file content
|
|
203
|
+
let content;
|
|
204
|
+
try {
|
|
205
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error(`[design-system-lint] Warning: ${e.message}`);
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Lint the content
|
|
212
|
+
const warnings = lintContent(content, filePath);
|
|
213
|
+
|
|
214
|
+
if (warnings.length > 0) {
|
|
215
|
+
const output = `
|
|
216
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
217
|
+
🎨 DESIGN SYSTEM LINT WARNINGS
|
|
218
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
219
|
+
|
|
220
|
+
File: ${path.basename(filePath)}
|
|
221
|
+
|
|
222
|
+
${warnings.map(w => `
|
|
223
|
+
⚠️ ${w.message}${w.count > 1 ? ` (${w.count} occurrences)` : ''}
|
|
224
|
+
→ ${w.suggestion}
|
|
225
|
+
${w.examples ? w.examples.map(e => ` Example: ${e}...`).join('\n') : ''}`).join('\n')}
|
|
226
|
+
|
|
227
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
228
|
+
📚 Reference: Load \`hailer-design-system\` skill for full guide
|
|
229
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
230
|
+
`;
|
|
231
|
+
console.error(output);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// CLI: Lint a specific file
|
|
238
|
+
if (process.argv[2] && !process.argv[2].startsWith('-')) {
|
|
239
|
+
const filePath = path.resolve(process.argv[2]);
|
|
240
|
+
|
|
241
|
+
if (!fs.existsSync(filePath)) {
|
|
242
|
+
console.error(`File not found: ${filePath}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
247
|
+
const warnings = lintContent(content, filePath);
|
|
248
|
+
|
|
249
|
+
if (warnings.length === 0) {
|
|
250
|
+
console.log(`✅ No design system issues found in ${path.basename(filePath)}`);
|
|
251
|
+
} else {
|
|
252
|
+
console.log(`Found ${warnings.length} issue(s) in ${path.basename(filePath)}:\n`);
|
|
253
|
+
for (const w of warnings) {
|
|
254
|
+
console.log(`⚠️ ${w.message}${w.count > 1 ? ` (${w.count}x)` : ''}`);
|
|
255
|
+
console.log(` → ${w.suggestion}\n`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// CLI: Help
|
|
262
|
+
if (process.argv[2] === '--help' || process.argv[2] === '-h') {
|
|
263
|
+
console.log(`
|
|
264
|
+
Design System Lint - Checks app code for Hailer Design System compliance
|
|
265
|
+
|
|
266
|
+
Usage:
|
|
267
|
+
node design-system-lint.cjs <file> Lint a specific file
|
|
268
|
+
node design-system-lint.cjs --help Show this help
|
|
269
|
+
|
|
270
|
+
Checks for:
|
|
271
|
+
- Hardcoded colors (hex, rgb, rgba)
|
|
272
|
+
- External icon libraries
|
|
273
|
+
- Plain HTML table elements
|
|
274
|
+
- Missing theme in ChakraProvider
|
|
275
|
+
- Inline styles with colors
|
|
276
|
+
- Manual stat card patterns
|
|
277
|
+
|
|
278
|
+
As a hook:
|
|
279
|
+
PostToolUse on Write/Edit
|
|
280
|
+
Outputs warnings to stderr (non-blocking)
|
|
281
|
+
`);
|
|
282
|
+
process.exit(0);
|
|
283
|
+
}
|
|
@@ -29,9 +29,17 @@
|
|
|
29
29
|
|
|
30
30
|
const path = require('path');
|
|
31
31
|
const fs = require('fs');
|
|
32
|
-
const
|
|
32
|
+
const os = require('os');
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// Skip in yolo mode
|
|
35
|
+
try {
|
|
36
|
+
const statePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', 'yolo-state.json');
|
|
37
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
38
|
+
if (state.mode === 'yolo') process.exit(0);
|
|
39
|
+
} catch {}
|
|
40
|
+
|
|
41
|
+
const TEMP_DIR = os.tmpdir();
|
|
42
|
+
const TRACKER_DIR = path.join(TEMP_DIR, '.claude-scaffolded-apps');
|
|
35
43
|
|
|
36
44
|
// Ensure tracker directory exists
|
|
37
45
|
if (!fs.existsSync(TRACKER_DIR)) {
|
|
@@ -122,6 +130,11 @@ You MUST use AskUserQuestion with EXACTLY this format:
|
|
|
122
130
|
IF USER SAYS YES: Follow these steps
|
|
123
131
|
----------------------------------------
|
|
124
132
|
|
|
133
|
+
0. Enable builder mode (required for Giuseppe to edit app files):
|
|
134
|
+
\`\`\`bash
|
|
135
|
+
node .claude/hooks/app-edit-guard.cjs --agent-on
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
125
138
|
1. Load the spawn-app-builder skill:
|
|
126
139
|
\`\`\`javascript
|
|
127
140
|
Skill("spawn-app-builder")
|
|
@@ -153,7 +166,7 @@ ASK THE USER NOW - Do not skip this question!
|
|
|
153
166
|
============================================================
|
|
154
167
|
`;
|
|
155
168
|
|
|
156
|
-
console.
|
|
169
|
+
console.log(JSON.stringify({ decision: 'allow', message: output }));
|
|
157
170
|
process.exit(0);
|
|
158
171
|
}
|
|
159
172
|
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* <hook-name>prompt-guard</hook-name>
|
|
4
|
+
*
|
|
5
|
+
* <purpose>
|
|
6
|
+
* Consolidated UserPromptSubmit hook. Replaces 4 separate hooks:
|
|
7
|
+
* - session-structure-check (project structure + auto-update hooks)
|
|
8
|
+
* - interactive-mode (suggest clarifying questions)
|
|
9
|
+
* - sync-marketplace-agents (update CLAUDE.md with marketplace agents)
|
|
10
|
+
* - git-hooks-check (notify about missing pre-commit hook)
|
|
11
|
+
*
|
|
12
|
+
* Single Node process instead of 4 = faster, no timeouts.
|
|
13
|
+
* </purpose>
|
|
14
|
+
*
|
|
15
|
+
* <triggers>UserPromptSubmit</triggers>
|
|
16
|
+
* @version 1.0.0
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
|
|
23
|
+
const ALLOW = JSON.stringify({ decision: 'allow' });
|
|
24
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
25
|
+
|
|
26
|
+
// --- Yolo mode check ---
|
|
27
|
+
let isYolo = false;
|
|
28
|
+
try {
|
|
29
|
+
const statePath = path.join(projectDir, '.claude', 'yolo-state.json');
|
|
30
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
31
|
+
isYolo = state.mode === 'yolo';
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
// Skip if stdin is TTY (no piped input)
|
|
35
|
+
if (process.stdin.isTTY) {
|
|
36
|
+
console.log(ALLOW);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Read stdin ---
|
|
41
|
+
let input = '';
|
|
42
|
+
process.stdin.setEncoding('utf8');
|
|
43
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
44
|
+
process.stdin.on('end', () => {
|
|
45
|
+
try {
|
|
46
|
+
const data = JSON.parse(input || '{}');
|
|
47
|
+
processHook(data);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error('[prompt-guard] CRASH:', e.message, e.stack);
|
|
50
|
+
console.log(ALLOW);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function processHook(data) {
|
|
56
|
+
const messages = [];
|
|
57
|
+
|
|
58
|
+
// 1. Session structure check (once per session)
|
|
59
|
+
try {
|
|
60
|
+
const structureMsg = checkSessionStructure();
|
|
61
|
+
if (structureMsg) messages.push(structureMsg);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error('[prompt-guard] checkSessionStructure crashed:', e.message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. Interactive mode (every message, skipped in yolo)
|
|
67
|
+
try {
|
|
68
|
+
if (!isYolo) {
|
|
69
|
+
const interactiveMsg = checkInteractiveMode(data.prompt);
|
|
70
|
+
if (interactiveMsg) messages.push(interactiveMsg);
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error('[prompt-guard] checkInteractiveMode crashed:', e.message);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. Sync marketplace agents (only when plugins changed)
|
|
77
|
+
try { syncMarketplaceAgents(); } catch (e) {
|
|
78
|
+
console.error('[prompt-guard] Marketplace sync crashed:', e.message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 4. Git hooks check (once per session, skipped in yolo)
|
|
82
|
+
try {
|
|
83
|
+
if (!isYolo) {
|
|
84
|
+
const gitMsg = checkGitHooks();
|
|
85
|
+
if (gitMsg) console.error(gitMsg);
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error('[prompt-guard] checkGitHooks crashed:', e.message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Output
|
|
92
|
+
if (messages.length > 0) {
|
|
93
|
+
console.log(JSON.stringify({
|
|
94
|
+
decision: 'allow',
|
|
95
|
+
message: messages.join('\n')
|
|
96
|
+
}));
|
|
97
|
+
} else {
|
|
98
|
+
console.log(ALLOW);
|
|
99
|
+
}
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================
|
|
104
|
+
// 1. Session Structure Check (from session-structure-check.cjs)
|
|
105
|
+
// ============================================================
|
|
106
|
+
function checkSessionStructure() {
|
|
107
|
+
// Find project root
|
|
108
|
+
let projectRoot = projectDir;
|
|
109
|
+
while (projectRoot !== '/' && !fs.existsSync(path.join(projectRoot, '.claude'))) {
|
|
110
|
+
projectRoot = path.dirname(projectRoot);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Session marker - only run once
|
|
114
|
+
const markerPath = path.join(projectRoot, '.claude', '.session-checked');
|
|
115
|
+
if (fs.existsSync(markerPath)) return null;
|
|
116
|
+
|
|
117
|
+
try { fs.writeFileSync(markerPath, new Date().toISOString()); } catch {}
|
|
118
|
+
|
|
119
|
+
const reminders = [];
|
|
120
|
+
|
|
121
|
+
const handoffPath = path.join(projectRoot, 'SESSION-HANDOFF.md');
|
|
122
|
+
const developmentMdPath = path.join(projectRoot, 'DEVELOPMENT.md');
|
|
123
|
+
const workspacePath = path.join(projectRoot, 'workspace');
|
|
124
|
+
|
|
125
|
+
if (fs.existsSync(handoffPath)) {
|
|
126
|
+
reminders.push('SESSION-HANDOFF.md found - read it to resume previous work, then update it.');
|
|
127
|
+
}
|
|
128
|
+
if (!fs.existsSync(developmentMdPath) && fs.existsSync(workspacePath)) {
|
|
129
|
+
reminders.push('No DEVELOPMENT.md found. Consider creating one to track project status.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Hooks are now project-local via $CLAUDE_PROJECT_DIR in settings.json
|
|
133
|
+
// No auto-update needed
|
|
134
|
+
|
|
135
|
+
if (reminders.length > 0) {
|
|
136
|
+
return '<session-start>\n' + reminders.join('\n') + '\n</session-start>';
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================
|
|
142
|
+
// 2. Interactive Mode (from interactive-mode.cjs)
|
|
143
|
+
// ============================================================
|
|
144
|
+
function checkInteractiveMode(prompt) {
|
|
145
|
+
if (!prompt) return null;
|
|
146
|
+
|
|
147
|
+
const taskPatterns = [
|
|
148
|
+
{ pattern: /(add|create).*(field|workflow|phase)/i, type: 'schema', questions: ['Field type?', 'Required or optional?', 'Default values?'] },
|
|
149
|
+
{ pattern: /(build|create|make).*(app|dashboard|portal|ui)/i, type: 'app', questions: ['What data to display?', 'What layout/components?', 'What user actions needed?'] },
|
|
150
|
+
{ pattern: /(create|add).*(insight|report)/i, type: 'insight', questions: ['What metrics/aggregations?', 'Which workflows to query?', 'Any filters needed?'] },
|
|
151
|
+
{ pattern: /(import|create).*(activit|record)|bulk/i, type: 'data', questions: ['Which workflow?', 'What field values?', 'How many records?'] },
|
|
152
|
+
{ pattern: /(update|change|modify).*(activit|record|field|data)/i, type: 'update', questions: ['Which records affected?', 'What new values?', 'Confirm before applying?'] },
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
// Feature implementation detection
|
|
156
|
+
const isFeatureRequest = /implement|build.*feature|add.*feature|new feature/i.test(prompt);
|
|
157
|
+
const planMarkers = (prompt.match(/step\s*\d|phase\s*\d|##|requirement|trigger|action/gi) || []).length;
|
|
158
|
+
const hasDetailedPlan = prompt.length > 500 || planMarkers >= 2;
|
|
159
|
+
|
|
160
|
+
if (isFeatureRequest && hasDetailedPlan) {
|
|
161
|
+
return `<interactive-mode>
|
|
162
|
+
FEATURE IMPLEMENTATION DETECTED with detailed plan.
|
|
163
|
+
|
|
164
|
+
Before implementing, ask: "Want me to create a PRD first?"
|
|
165
|
+
- PRDs track requirements in docs/prd-*.md
|
|
166
|
+
- Links to DEVELOPMENT.md roadmap
|
|
167
|
+
- Skip only if user explicitly declines
|
|
168
|
+
|
|
169
|
+
Use AskUserQuestion to offer PRD creation.
|
|
170
|
+
</interactive-mode>`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const matched = taskPatterns.find(p => p.pattern.test(prompt));
|
|
174
|
+
if (matched) {
|
|
175
|
+
return `<interactive-mode>
|
|
176
|
+
BEFORE STARTING: Consider asking clarifying questions.
|
|
177
|
+
|
|
178
|
+
Task type detected: ${matched.type}
|
|
179
|
+
Suggested questions to ask user:
|
|
180
|
+
${matched.questions.map(q => `- ${q}`).join('\n')}
|
|
181
|
+
|
|
182
|
+
Use AskUserQuestion tool if requirements are unclear.
|
|
183
|
+
Gather specifics before spawning agents or making changes.
|
|
184
|
+
</interactive-mode>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================================
|
|
191
|
+
// 3. Sync Marketplace Agents (from sync-marketplace-agents.cjs)
|
|
192
|
+
// ============================================================
|
|
193
|
+
function syncMarketplaceAgents() {
|
|
194
|
+
const crypto = require('crypto');
|
|
195
|
+
|
|
196
|
+
const PLUGINS_DIR = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces');
|
|
197
|
+
const INSTALLED_PLUGINS = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
198
|
+
const CLAUDE_MD = path.join(projectDir, 'CLAUDE.md');
|
|
199
|
+
const SYNC_STATE_DIR = path.join(os.homedir(), '.claude', 'sync-state');
|
|
200
|
+
const PROJECT_HASH = crypto.createHash('md5').update(projectDir).digest('hex').slice(0, 12);
|
|
201
|
+
const SYNC_STATE = path.join(SYNC_STATE_DIR, `${PROJECT_HASH}.state`);
|
|
202
|
+
|
|
203
|
+
function getInstalledPluginsHash() {
|
|
204
|
+
if (!fs.existsSync(INSTALLED_PLUGINS)) return 'empty';
|
|
205
|
+
return crypto.createHash('md5').update(fs.readFileSync(INSTALLED_PLUGINS, 'utf-8')).digest('hex');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check if plugins changed
|
|
209
|
+
const currentHash = getInstalledPluginsHash();
|
|
210
|
+
try {
|
|
211
|
+
if (fs.existsSync(SYNC_STATE) && fs.readFileSync(SYNC_STATE, 'utf-8').trim() === currentHash) {
|
|
212
|
+
return; // No change
|
|
213
|
+
}
|
|
214
|
+
} catch {}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const installedPlugins = getInstalledPlugins(INSTALLED_PLUGINS);
|
|
218
|
+
const agents = scanMarketplaceAgents(PLUGINS_DIR, installedPlugins);
|
|
219
|
+
updateClaudeMd(CLAUDE_MD, agents);
|
|
220
|
+
|
|
221
|
+
// Save sync state
|
|
222
|
+
fs.mkdirSync(SYNC_STATE_DIR, { recursive: true });
|
|
223
|
+
fs.writeFileSync(SYNC_STATE, currentHash);
|
|
224
|
+
console.error(`[prompt-guard] Detected plugin changes, found ${agents.length} marketplace agents`);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error(`[prompt-guard] Sync agents warning: ${e.message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getInstalledPlugins(installedPluginsPath) {
|
|
231
|
+
const plugins = new Set();
|
|
232
|
+
if (!fs.existsSync(installedPluginsPath)) return plugins;
|
|
233
|
+
try {
|
|
234
|
+
const data = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
|
|
235
|
+
if (data.plugins) for (const key of Object.keys(data.plugins)) plugins.add(key);
|
|
236
|
+
} catch {}
|
|
237
|
+
return plugins;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function scanMarketplaceAgents(pluginsDir, enabledPlugins) {
|
|
241
|
+
const agents = [];
|
|
242
|
+
if (!fs.existsSync(pluginsDir)) return agents;
|
|
243
|
+
|
|
244
|
+
for (const marketplace of fs.readdirSync(pluginsDir)) {
|
|
245
|
+
const marketplacePath = path.join(pluginsDir, marketplace);
|
|
246
|
+
if (!fs.statSync(marketplacePath).isDirectory()) continue;
|
|
247
|
+
|
|
248
|
+
// Flat structure: {marketplace}/agents/
|
|
249
|
+
const flatAgentsPath = path.join(marketplacePath, 'agents');
|
|
250
|
+
if (fs.existsSync(flatAgentsPath)) {
|
|
251
|
+
const pluginKey = `${marketplace}@${marketplace}`;
|
|
252
|
+
if (enabledPlugins.has(pluginKey)) {
|
|
253
|
+
for (const f of fs.readdirSync(flatAgentsPath).filter(f => f.endsWith('.md'))) {
|
|
254
|
+
const content = fs.readFileSync(path.join(flatAgentsPath, f), 'utf-8');
|
|
255
|
+
const fm = parseFrontmatter(content);
|
|
256
|
+
agents.push({ marketplace, plugin: marketplace, name: path.basename(f, '.md'), fullName: `${marketplace}:${path.basename(f, '.md')}`, description: fm.description || '', model: fm.model || 'sonnet' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Root-level plugins: {marketplace}/{plugin}/agents/
|
|
262
|
+
for (const item of fs.readdirSync(marketplacePath)) {
|
|
263
|
+
if (item === '.claude-plugin' || item === '.git' || item === 'plugins') continue;
|
|
264
|
+
const itemPath = path.join(marketplacePath, item);
|
|
265
|
+
if (!fs.statSync(itemPath).isDirectory()) continue;
|
|
266
|
+
if (!fs.existsSync(path.join(itemPath, '.claude-plugin', 'plugin.json'))) continue;
|
|
267
|
+
if (!enabledPlugins.has(`${item}@${marketplace}`)) continue;
|
|
268
|
+
const agentsPath = path.join(itemPath, 'agents');
|
|
269
|
+
if (!fs.existsSync(agentsPath)) continue;
|
|
270
|
+
for (const f of fs.readdirSync(agentsPath).filter(f => f.endsWith('.md'))) {
|
|
271
|
+
const content = fs.readFileSync(path.join(agentsPath, f), 'utf-8');
|
|
272
|
+
const fm = parseFrontmatter(content);
|
|
273
|
+
agents.push({ marketplace, plugin: item, name: path.basename(f, '.md'), fullName: `${item}:${path.basename(f, '.md')}`, description: fm.description || '', model: fm.model || 'sonnet' });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Nested: {marketplace}/plugins/{plugin}/agents/
|
|
278
|
+
const nestedPath = path.join(marketplacePath, 'plugins');
|
|
279
|
+
if (fs.existsSync(nestedPath)) {
|
|
280
|
+
for (const plugin of fs.readdirSync(nestedPath)) {
|
|
281
|
+
if (!enabledPlugins.has(`${plugin}@${marketplace}`)) continue;
|
|
282
|
+
const pluginPath = path.join(nestedPath, plugin);
|
|
283
|
+
if (!fs.statSync(pluginPath).isDirectory()) continue;
|
|
284
|
+
const agentsPath = path.join(pluginPath, 'agents');
|
|
285
|
+
if (!fs.existsSync(agentsPath)) continue;
|
|
286
|
+
for (const f of fs.readdirSync(agentsPath).filter(f => f.endsWith('.md'))) {
|
|
287
|
+
const content = fs.readFileSync(path.join(agentsPath, f), 'utf-8');
|
|
288
|
+
const fm = parseFrontmatter(content);
|
|
289
|
+
agents.push({ marketplace, plugin, name: path.basename(f, '.md'), fullName: `${plugin}:${path.basename(f, '.md')}`, description: fm.description || '', model: fm.model || 'sonnet' });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return agents;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseFrontmatter(content) {
|
|
298
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
299
|
+
if (!match) return {};
|
|
300
|
+
const fm = {};
|
|
301
|
+
for (const line of match[1].split('\n')) {
|
|
302
|
+
const i = line.indexOf(':');
|
|
303
|
+
if (i <= 0 || line.startsWith(' ') || line.startsWith('\t')) continue;
|
|
304
|
+
const key = line.slice(0, i).trim();
|
|
305
|
+
let value = line.slice(i + 1).trim();
|
|
306
|
+
if (key === 'description') value = value.replace(/\\n.*/g, '').slice(0, 100);
|
|
307
|
+
fm[key] = value;
|
|
308
|
+
}
|
|
309
|
+
return fm;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function updateClaudeMd(claudeMdPath, agents) {
|
|
313
|
+
if (!fs.existsSync(claudeMdPath)) return;
|
|
314
|
+
let content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
315
|
+
|
|
316
|
+
let table = agents.length === 0 ? 'No marketplace agents installed.' :
|
|
317
|
+
'| Agent | Plugin | Model | Description |\n|-------|--------|-------|-------------|\n' +
|
|
318
|
+
agents.map(a => `| \`${a.fullName}\` | ${a.plugin} | ${a.model} | ${a.description.slice(0, 50)}${a.description.length > 50 ? '...' : ''} |`).join('\n');
|
|
319
|
+
|
|
320
|
+
const sectionStart = content.indexOf('<config-source>');
|
|
321
|
+
const sectionEnd = content.indexOf('</config-source>');
|
|
322
|
+
if (sectionStart === -1 || sectionEnd === -1) return;
|
|
323
|
+
|
|
324
|
+
const tableStart = content.indexOf('| Agent |', sectionStart);
|
|
325
|
+
const noAgents = content.indexOf('No marketplace agents installed.', sectionStart);
|
|
326
|
+
|
|
327
|
+
let replaceStart, replaceEnd;
|
|
328
|
+
if (tableStart !== -1 && tableStart < sectionEnd) { replaceStart = tableStart; replaceEnd = sectionEnd; }
|
|
329
|
+
else if (noAgents !== -1 && noAgents < sectionEnd) { replaceStart = noAgents; replaceEnd = noAgents + 'No marketplace agents installed.'.length; }
|
|
330
|
+
else { replaceStart = content.indexOf('\n', sectionStart) + 1; replaceEnd = replaceStart; }
|
|
331
|
+
|
|
332
|
+
content = content.substring(0, replaceStart) + table + '\n\n' + content.substring(replaceEnd);
|
|
333
|
+
fs.writeFileSync(claudeMdPath, content);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ============================================================
|
|
337
|
+
// 4. Git Hooks Check (from git-hooks-check.cjs)
|
|
338
|
+
// ============================================================
|
|
339
|
+
function checkGitHooks() {
|
|
340
|
+
const SESSION_FILE = path.join(os.tmpdir(), '.claude-git-hooks-checked');
|
|
341
|
+
const SESSION_TTL = 3600000;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
345
|
+
const data = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
346
|
+
if (data.projectDir === projectDir && (Date.now() - data.checkedAt) < SESSION_TTL) return null;
|
|
347
|
+
}
|
|
348
|
+
} catch {}
|
|
349
|
+
|
|
350
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify({ projectDir, checkedAt: Date.now() }));
|
|
351
|
+
|
|
352
|
+
const gitDir = path.join(projectDir, '.git');
|
|
353
|
+
if (!fs.existsSync(gitDir)) return null;
|
|
354
|
+
|
|
355
|
+
const hookSource = path.join(projectDir, '.claude', 'hooks', 'pre-commit-test.sh');
|
|
356
|
+
if (!fs.existsSync(hookSource)) return null;
|
|
357
|
+
|
|
358
|
+
const preCommitHook = path.join(gitDir, 'hooks', 'pre-commit');
|
|
359
|
+
if (fs.existsSync(preCommitHook)) return null;
|
|
360
|
+
|
|
361
|
+
return `
|
|
362
|
+
Git pre-commit hook not installed.
|
|
363
|
+
Install: ln -sf ../../.claude/hooks/pre-commit-test.sh .git/hooks/pre-commit
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
|