@hailer/mcp 0.0.6 → 0.1.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/.claude/agents/ada.md +127 -0
- package/.claude/agents/agent-builder.md +151 -0
- package/.claude/agents/alejandro.md +66 -0
- package/.claude/agents/bjorn.md +305 -0
- package/.claude/agents/dmitri.md +61 -0
- package/.claude/agents/giuseppe.md +66 -0
- package/.claude/agents/gunther.md +355 -0
- package/.claude/agents/helga.md +68 -0
- package/.claude/agents/ingrid.md +108 -0
- package/.claude/agents/kenji.md +58 -0
- package/.claude/agents/svetlana.md +394 -0
- package/.claude/agents/viktor.md +63 -0
- package/.claude/agents/yevgeni.md +60 -0
- package/.claude/hooks/agent-failure-detector.cjs +286 -0
- package/.claude/hooks/app-edit-guard.cjs +462 -0
- package/.claude/hooks/interactive-mode.cjs +59 -0
- package/.claude/hooks/mcp-server-guard.cjs +92 -0
- package/.claude/hooks/post-scaffold-hook.cjs +31 -0
- package/.claude/hooks/sdk-delete-guard.cjs +2 -0
- package/.claude/hooks/src-edit-guard.cjs +208 -0
- package/.claude/settings.json +47 -2
- package/.claude/skills/insight-join-patterns/SKILL.md +209 -0
- package/.env.example +13 -1
- package/CLAUDE.md +135 -0
- package/dist/app.js +4 -3
- package/dist/cli.js +0 -0
- package/dist/client/adaptive-documentation-bot.d.ts +0 -2
- package/dist/client/adaptive-documentation-bot.js +5 -16
- package/dist/client/message-processor.js +5 -0
- package/dist/client/providers/anthropic-provider.js +21 -7
- package/dist/mcp/UserContextCache.d.ts +14 -0
- package/dist/mcp/UserContextCache.js +49 -24
- package/dist/mcp/auth.d.ts +7 -0
- package/dist/mcp/auth.js +13 -5
- package/dist/mcp/hailer-clients.d.ts +5 -2
- package/dist/mcp/signal-handler.d.ts +28 -2
- package/dist/mcp/signal-handler.js +4 -2
- package/dist/mcp/tool-registry.d.ts +55 -2
- package/dist/mcp/tool-registry.js +197 -2
- package/dist/mcp/tools/app-core.d.ts +15 -0
- package/dist/mcp/tools/app-core.js +609 -0
- package/dist/mcp/tools/app-marketplace.d.ts +21 -0
- package/dist/mcp/tools/app-marketplace.js +1284 -0
- package/dist/mcp/tools/app-member.d.ts +11 -0
- package/dist/mcp/tools/app-member.js +258 -0
- package/dist/mcp/tools/app-scaffold.d.ts +11 -0
- package/dist/mcp/tools/app-scaffold.js +743 -0
- package/dist/mcp/tools/app.d.ts +13 -22
- package/dist/mcp/tools/app.js +17 -2466
- package/dist/mcp/tools/file.js +6 -6
- package/dist/mcp/tools/insight.d.ts +1 -0
- package/dist/mcp/tools/insight.js +203 -64
- package/dist/mcp/tools/user.js +3 -9
- package/dist/mcp/tools/workflow.js +49 -38
- package/dist/mcp/utils/hailer-api-client.js +4 -13
- package/dist/mcp/utils/tool-helpers.d.ts +102 -0
- package/dist/mcp/utils/tool-helpers.js +179 -0
- package/dist/mcp/utils/types.d.ts +6 -0
- package/dist/mcp/workspace-cache.d.ts +5 -5
- package/dist/mcp/workspace-cache.js +4 -3
- package/package.json +1 -1
- package/.claude/hooks/PreToolUse.sh +0 -52
- package/.claude/hooks/prompt-skill-loader.cjs +0 -553
- package/.claude/hooks/skill-loader.cjs +0 -142
- package/.claude/settings.local.json +0 -49
- package/.claude/skills/MCP-add-app-member-skill/SKILL.md +0 -977
- package/.claude/skills/MCP-build-data-app-skill/SKILL.md +0 -372
- package/.claude/skills/MCP-create-app-skill/SKILL.md +0 -1101
- package/.claude/skills/MCP-create-insight-skill/SKILL.md +0 -1317
- package/.claude/skills/MCP-get-insight-data-skill/SKILL.md +0 -1053
- package/.claude/skills/MCP-insight-api/SKILL.md +0 -185
- package/.claude/skills/MCP-insight-api/references/insight-endpoints.md +0 -514
- package/.claude/skills/MCP-install-workflow-skill/SKILL.md +0 -1056
- package/.claude/skills/MCP-list-apps-skill/SKILL.md +0 -1010
- package/.claude/skills/MCP-list-workflows-minimal-skill/SKILL.md +0 -992
- package/.claude/skills/MCP-local-first-skill/SKILL.md +0 -570
- package/.claude/skills/MCP-populate-workflow-data-skill/SKILL.md +0 -395
- package/.claude/skills/MCP-preview-insight-skill/SKILL.md +0 -1290
- package/.claude/skills/MCP-publish-hailer-app-skill/SKILL.md +0 -453
- package/.claude/skills/MCP-publish-template-skill/SKILL.md +0 -278
- package/.claude/skills/MCP-remove-app-member-skill/SKILL.md +0 -671
- package/.claude/skills/MCP-remove-app-skill/SKILL.md +0 -985
- package/.claude/skills/MCP-remove-insight-skill/SKILL.md +0 -1011
- package/.claude/skills/MCP-remove-workflow-skill/SKILL.md +0 -920
- package/.claude/skills/MCP-scaffold-hailer-app-skill/SKILL.md +0 -1314
- package/.claude/skills/MCP-update-app-skill/SKILL.md +0 -970
- package/.claude/skills/MCP-update-workflow-field-skill/SKILL.md +0 -1098
- package/.claude/skills/SDK-create-function-field-skill/SKILL.md +0 -313
- package/.claude/skills/SDK-generate-skill/SKILL.md +0 -223
- package/.claude/skills/SDK-init-skill/SKILL.md +0 -177
- package/.claude/skills/SDK-workspace-setup-skill/SKILL.md +0 -605
- package/.claude/skills/SDK-ws-config-skill/SKILL.md +0 -435
- package/.claude/skills/activity-api/SKILL.md +0 -96
- package/.claude/skills/activity-api/references/activity-endpoints.md +0 -845
- package/.claude/skills/agent-building/SKILL.md +0 -243
- package/.claude/skills/agent-building/references/architecture-patterns.md +0 -446
- package/.claude/skills/agent-building/references/code-examples.md +0 -587
- package/.claude/skills/agent-building/references/implementation-guide.md +0 -619
- package/.claude/skills/app-api/SKILL.md +0 -219
- package/.claude/skills/app-api/references/app-endpoints.md +0 -759
- package/.claude/skills/building-hailer-apps-skill/SKILL.md +0 -813
- package/.claude/skills/hailer-api/SKILL.md +0 -283
- package/.claude/skills/hailer-api/references/activities.md +0 -620
- package/.claude/skills/hailer-api/references/authentication.md +0 -216
- package/.claude/skills/hailer-api/references/datasets.md +0 -437
- package/.claude/skills/hailer-api/references/files.md +0 -301
- package/.claude/skills/hailer-api/references/insights.md +0 -469
- package/.claude/skills/hailer-api/references/workflows.md +0 -720
- package/.claude/skills/hailer-api/references/workspaces-users.md +0 -445
- package/.claude/skills/hailer-app-builder/SKILL.md +0 -340
- package/.claude/skills/mcp-tools/SKILL.md +0 -419
- package/.claude/skills/mcp-tools/references/api-endpoints.md +0 -499
- package/.claude/skills/mcp-tools/references/data-structures.md +0 -554
- package/.claude/skills/mcp-tools/references/implementation-patterns.md +0 -717
- package/.claude/skills/skill-testing/README.md +0 -137
- package/.claude/skills/skill-testing/SKILL.md +0 -348
- package/.claude/skills/skill-testing/references/test-patterns.md +0 -705
- package/.claude/skills/skill-testing/references/testing-guide.md +0 -603
- package/.claude/skills/skill-testing/references/validation-checklist.md +0 -537
- package/.claude/skills/spawn-app-builder/SKILL.md +0 -366
- package/.claude/skills/tool-builder/SKILL.md +0 -328
- package/tsconfig.json +0 -23
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code PreToolUse Hook - BULLETPROOF Hailer App Guard
|
|
4
|
+
*
|
|
5
|
+
* Blocks ALL direct edits to Hailer app directories.
|
|
6
|
+
* Detection is pattern-based - no registration required.
|
|
7
|
+
*
|
|
8
|
+
* A directory is considered a Hailer app if it contains:
|
|
9
|
+
* - public/manifest.json with "appId" field, OR
|
|
10
|
+
* - package.json with @hailer/app-sdk dependency
|
|
11
|
+
*
|
|
12
|
+
* Edits are ONLY allowed when:
|
|
13
|
+
* 1. Running inside a subagent (Task tool with CLAUDE_CODE_ENTRYPOINT=task)
|
|
14
|
+
* 2. The app directory has been explicitly released for manual editing
|
|
15
|
+
*
|
|
16
|
+
* To release an app for manual editing:
|
|
17
|
+
* node app-edit-guard.cjs --release /path/to/app
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const RELEASE_TRACKER = '/tmp/.claude-released-apps.json';
|
|
24
|
+
const BUILDER_MODE_DIR = '/tmp/.claude-builder-mode';
|
|
25
|
+
const BUILDER_AGENT_ACTIVE = '/tmp/.claude-builder-agent-active';
|
|
26
|
+
|
|
27
|
+
// Read hook input from stdin
|
|
28
|
+
let input = '';
|
|
29
|
+
process.stdin.setEncoding('utf8');
|
|
30
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
31
|
+
process.stdin.on('end', () => {
|
|
32
|
+
try {
|
|
33
|
+
const data = JSON.parse(input);
|
|
34
|
+
processHook(data);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Invalid JSON - fail safe by BLOCKING
|
|
37
|
+
outputBlock('Hook received invalid input - blocking for safety');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function outputAllow() {
|
|
42
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function outputBlock(message) {
|
|
47
|
+
console.log(JSON.stringify({
|
|
48
|
+
decision: 'block',
|
|
49
|
+
reason: message
|
|
50
|
+
}));
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a directory is a Hailer app by examining its contents
|
|
56
|
+
*/
|
|
57
|
+
function isHailerAppDirectory(dirPath) {
|
|
58
|
+
try {
|
|
59
|
+
// Check 1: public/manifest.json with appId
|
|
60
|
+
const manifestPath = path.join(dirPath, 'public', 'manifest.json');
|
|
61
|
+
if (fs.existsSync(manifestPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
64
|
+
if (manifest.appId || manifest.name) {
|
|
65
|
+
return { isApp: true, name: manifest.name || path.basename(dirPath), reason: 'manifest.json' };
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Invalid JSON in manifest, still might be a Hailer app structure
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check 2: package.json with @hailer/app-sdk
|
|
73
|
+
const packagePath = path.join(dirPath, 'package.json');
|
|
74
|
+
if (fs.existsSync(packagePath)) {
|
|
75
|
+
try {
|
|
76
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
77
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
78
|
+
if (deps['@hailer/app-sdk']) {
|
|
79
|
+
return { isApp: true, name: pkg.name || path.basename(dirPath), reason: '@hailer/app-sdk dependency' };
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Invalid package.json
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check 3: vite.config with hailer in comments or specific CORS config
|
|
87
|
+
const viteConfigPath = path.join(dirPath, 'vite.config.ts');
|
|
88
|
+
if (fs.existsSync(viteConfigPath)) {
|
|
89
|
+
try {
|
|
90
|
+
const viteConfig = fs.readFileSync(viteConfigPath, 'utf8');
|
|
91
|
+
if (viteConfig.includes('hailer') || viteConfig.includes('app.hailer.com')) {
|
|
92
|
+
return { isApp: true, name: path.basename(dirPath), reason: 'vite.config.ts hailer reference' };
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Can't read vite config
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { isApp: false };
|
|
100
|
+
} catch {
|
|
101
|
+
return { isApp: false };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Walk up the directory tree to find if file is inside a Hailer app
|
|
107
|
+
*/
|
|
108
|
+
function findHailerAppRoot(filePath) {
|
|
109
|
+
let currentDir = path.dirname(filePath);
|
|
110
|
+
const root = path.parse(currentDir).root;
|
|
111
|
+
|
|
112
|
+
// Walk up max 10 levels
|
|
113
|
+
for (let i = 0; i < 10 && currentDir !== root; i++) {
|
|
114
|
+
const result = isHailerAppDirectory(currentDir);
|
|
115
|
+
if (result.isApp) {
|
|
116
|
+
return { ...result, path: currentDir };
|
|
117
|
+
}
|
|
118
|
+
currentDir = path.dirname(currentDir);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if an app directory has been released for manual editing
|
|
126
|
+
*/
|
|
127
|
+
function isAppReleased(appPath) {
|
|
128
|
+
try {
|
|
129
|
+
if (!fs.existsSync(RELEASE_TRACKER)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const released = JSON.parse(fs.readFileSync(RELEASE_TRACKER, 'utf8'));
|
|
133
|
+
return released.includes(path.resolve(appPath));
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a builder agent is currently active (global flag)
|
|
141
|
+
* Main Claude enables this before spawning builder agents
|
|
142
|
+
*/
|
|
143
|
+
function isBuilderAgentActive() {
|
|
144
|
+
return fs.existsSync(BUILDER_AGENT_ACTIVE);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if builder mode is active for an app directory
|
|
149
|
+
* Builder mode is enabled by creating a marker file in BUILDER_MODE_DIR
|
|
150
|
+
*/
|
|
151
|
+
function isBuilderModeActive(appPath) {
|
|
152
|
+
try {
|
|
153
|
+
if (!fs.existsSync(BUILDER_MODE_DIR)) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
// Hash the app path to create a unique marker filename
|
|
157
|
+
const normalizedPath = path.resolve(appPath);
|
|
158
|
+
const markerFile = path.join(BUILDER_MODE_DIR, Buffer.from(normalizedPath).toString('base64').replace(/[/+=]/g, '_'));
|
|
159
|
+
return fs.existsSync(markerFile);
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Enable builder mode for an app directory
|
|
167
|
+
*/
|
|
168
|
+
function enableBuilderMode(appPath) {
|
|
169
|
+
const normalizedPath = path.resolve(appPath);
|
|
170
|
+
if (!fs.existsSync(BUILDER_MODE_DIR)) {
|
|
171
|
+
fs.mkdirSync(BUILDER_MODE_DIR, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
const markerFile = path.join(BUILDER_MODE_DIR, Buffer.from(normalizedPath).toString('base64').replace(/[/+=]/g, '_'));
|
|
174
|
+
fs.writeFileSync(markerFile, JSON.stringify({ appPath: normalizedPath, enabledAt: new Date().toISOString() }));
|
|
175
|
+
return markerFile;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Disable builder mode for an app directory
|
|
180
|
+
*/
|
|
181
|
+
function disableBuilderMode(appPath) {
|
|
182
|
+
const normalizedPath = path.resolve(appPath);
|
|
183
|
+
const markerFile = path.join(BUILDER_MODE_DIR, Buffer.from(normalizedPath).toString('base64').replace(/[/+=]/g, '_'));
|
|
184
|
+
if (fs.existsSync(markerFile)) {
|
|
185
|
+
fs.unlinkSync(markerFile);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function processHook(data) {
|
|
192
|
+
const { tool_name, tool_input } = data;
|
|
193
|
+
|
|
194
|
+
// Only guard Write and Edit tools
|
|
195
|
+
if (tool_name !== 'Write' && tool_name !== 'Edit') {
|
|
196
|
+
outputAllow();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const filePath = tool_input?.file_path;
|
|
201
|
+
if (!filePath) {
|
|
202
|
+
outputAllow();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Normalize path
|
|
207
|
+
const normalizedPath = path.resolve(filePath);
|
|
208
|
+
|
|
209
|
+
// Find if this file is inside a Hailer app
|
|
210
|
+
const appInfo = findHailerAppRoot(normalizedPath);
|
|
211
|
+
|
|
212
|
+
if (!appInfo) {
|
|
213
|
+
// Not in a Hailer app directory
|
|
214
|
+
outputAllow();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check if a builder agent is globally active - ALLOW
|
|
219
|
+
// Main Claude enables this before spawning builder agents
|
|
220
|
+
if (isBuilderAgentActive()) {
|
|
221
|
+
outputAllow();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check if builder mode is active for this app - ALLOW
|
|
226
|
+
// This is the primary mechanism for spawned builder agents
|
|
227
|
+
if (isBuilderModeActive(appInfo.path)) {
|
|
228
|
+
outputAllow();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check if this app has been released for manual editing - ALLOW
|
|
233
|
+
if (isAppReleased(appInfo.path)) {
|
|
234
|
+
outputAllow();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// BLOCK with helpful message
|
|
239
|
+
outputBlock(`
|
|
240
|
+
🚫 BLOCKED: Direct edit to Hailer app "${appInfo.name}"
|
|
241
|
+
|
|
242
|
+
Detected as Hailer app via: ${appInfo.reason}
|
|
243
|
+
App directory: ${appInfo.path}
|
|
244
|
+
|
|
245
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
246
|
+
WHY: Hailer apps must be built by a specialized builder agent
|
|
247
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
248
|
+
|
|
249
|
+
The builder agent has:
|
|
250
|
+
• Access to @hailer/app-sdk documentation
|
|
251
|
+
• Live dev server feedback for iteration
|
|
252
|
+
• Proper TypeScript patterns for Hailer apps
|
|
253
|
+
|
|
254
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
255
|
+
TO PROCEED: Spawn a builder agent
|
|
256
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
257
|
+
|
|
258
|
+
1. Load the spawn skill:
|
|
259
|
+
Skill("spawn-app-builder")
|
|
260
|
+
|
|
261
|
+
2. Follow the skill instructions to spawn the agent
|
|
262
|
+
|
|
263
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
264
|
+
OR: Release for manual editing (if user explicitly requests)
|
|
265
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
266
|
+
|
|
267
|
+
Ask the user first! If they confirm manual editing, run:
|
|
268
|
+
Bash: node .claude/hooks/app-edit-guard.cjs --release "${appInfo.path}"
|
|
269
|
+
|
|
270
|
+
Then retry your edit.
|
|
271
|
+
`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// CLI: Release an app for manual editing
|
|
275
|
+
if (process.argv[2] === '--release' && process.argv[3]) {
|
|
276
|
+
const appPath = path.resolve(process.argv[3]);
|
|
277
|
+
|
|
278
|
+
// Verify it's actually a Hailer app
|
|
279
|
+
const appInfo = isHailerAppDirectory(appPath);
|
|
280
|
+
if (!appInfo.isApp) {
|
|
281
|
+
console.error(`Error: ${appPath} is not a Hailer app directory`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Load existing releases
|
|
286
|
+
let released = [];
|
|
287
|
+
try {
|
|
288
|
+
if (fs.existsSync(RELEASE_TRACKER)) {
|
|
289
|
+
released = JSON.parse(fs.readFileSync(RELEASE_TRACKER, 'utf8'));
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
released = [];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Add this app if not already released
|
|
296
|
+
if (!released.includes(appPath)) {
|
|
297
|
+
released.push(appPath);
|
|
298
|
+
fs.writeFileSync(RELEASE_TRACKER, JSON.stringify(released, null, 2));
|
|
299
|
+
console.log(`✅ Released "${appInfo.name}" for manual editing`);
|
|
300
|
+
console.log(` Path: ${appPath}`);
|
|
301
|
+
} else {
|
|
302
|
+
console.log(`ℹ️ "${appInfo.name}" was already released`);
|
|
303
|
+
}
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// CLI: List released apps
|
|
308
|
+
if (process.argv[2] === '--list') {
|
|
309
|
+
try {
|
|
310
|
+
if (fs.existsSync(RELEASE_TRACKER)) {
|
|
311
|
+
const released = JSON.parse(fs.readFileSync(RELEASE_TRACKER, 'utf8'));
|
|
312
|
+
if (released.length === 0) {
|
|
313
|
+
console.log('No apps released for manual editing');
|
|
314
|
+
} else {
|
|
315
|
+
console.log('Released apps:');
|
|
316
|
+
released.forEach(p => console.log(` - ${p}`));
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
console.log('No apps released for manual editing');
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
console.log('No apps released for manual editing');
|
|
323
|
+
}
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// CLI: Enable builder mode for an app
|
|
328
|
+
if (process.argv[2] === '--builder-on' && process.argv[3]) {
|
|
329
|
+
const appPath = path.resolve(process.argv[3]);
|
|
330
|
+
|
|
331
|
+
// Verify it's actually a Hailer app
|
|
332
|
+
const appInfo = isHailerAppDirectory(appPath);
|
|
333
|
+
if (!appInfo.isApp) {
|
|
334
|
+
console.error(`Error: ${appPath} is not a Hailer app directory`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
enableBuilderMode(appPath);
|
|
339
|
+
console.log(`🔧 Builder mode ENABLED for "${appInfo.name}"`);
|
|
340
|
+
console.log(` Path: ${appPath}`);
|
|
341
|
+
console.log(` Spawned agents can now edit this app`);
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// CLI: Disable builder mode for an app
|
|
346
|
+
if (process.argv[2] === '--builder-off' && process.argv[3]) {
|
|
347
|
+
const appPath = path.resolve(process.argv[3]);
|
|
348
|
+
|
|
349
|
+
if (disableBuilderMode(appPath)) {
|
|
350
|
+
console.log(`🔒 Builder mode DISABLED for: ${appPath}`);
|
|
351
|
+
} else {
|
|
352
|
+
console.log(`ℹ️ Builder mode was not active for: ${appPath}`);
|
|
353
|
+
}
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// CLI: Revoke release
|
|
358
|
+
if (process.argv[2] === '--revoke' && process.argv[3]) {
|
|
359
|
+
const appPath = path.resolve(process.argv[3]);
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
if (fs.existsSync(RELEASE_TRACKER)) {
|
|
363
|
+
let released = JSON.parse(fs.readFileSync(RELEASE_TRACKER, 'utf8'));
|
|
364
|
+
const before = released.length;
|
|
365
|
+
released = released.filter(p => p !== appPath);
|
|
366
|
+
if (released.length < before) {
|
|
367
|
+
fs.writeFileSync(RELEASE_TRACKER, JSON.stringify(released, null, 2));
|
|
368
|
+
console.log(`✅ Revoked manual editing for: ${appPath}`);
|
|
369
|
+
} else {
|
|
370
|
+
console.log(`ℹ️ App was not in released list: ${appPath}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
console.error('Error reading release tracker');
|
|
375
|
+
}
|
|
376
|
+
process.exit(0);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// CLI: Check if a path is a Hailer app
|
|
380
|
+
if (process.argv[2] === '--check' && process.argv[3]) {
|
|
381
|
+
const checkPath = path.resolve(process.argv[3]);
|
|
382
|
+
const result = isHailerAppDirectory(checkPath);
|
|
383
|
+
if (result.isApp) {
|
|
384
|
+
console.log(`✅ Hailer app detected: ${result.name}`);
|
|
385
|
+
console.log(` Detected via: ${result.reason}`);
|
|
386
|
+
console.log(` Builder mode: ${isBuilderModeActive(checkPath) ? 'ACTIVE' : 'Off'}`);
|
|
387
|
+
console.log(` Manual release: ${isAppReleased(checkPath) ? 'Yes' : 'No'}`);
|
|
388
|
+
} else {
|
|
389
|
+
console.log(`❌ Not a Hailer app: ${checkPath}`);
|
|
390
|
+
}
|
|
391
|
+
process.exit(0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// CLI: Enable global builder agent mode
|
|
395
|
+
if (process.argv[2] === '--agent-on') {
|
|
396
|
+
fs.writeFileSync(BUILDER_AGENT_ACTIVE, JSON.stringify({ enabledAt: new Date().toISOString() }));
|
|
397
|
+
console.log('🔧 Builder agent mode ENABLED globally');
|
|
398
|
+
console.log(' All Hailer app edits are now allowed');
|
|
399
|
+
console.log(' Run --agent-off when done');
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// CLI: Disable global builder agent mode
|
|
404
|
+
if (process.argv[2] === '--agent-off') {
|
|
405
|
+
if (fs.existsSync(BUILDER_AGENT_ACTIVE)) {
|
|
406
|
+
fs.unlinkSync(BUILDER_AGENT_ACTIVE);
|
|
407
|
+
console.log('🔒 Builder agent mode DISABLED');
|
|
408
|
+
} else {
|
|
409
|
+
console.log('ℹ️ Builder agent mode was not active');
|
|
410
|
+
}
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// CLI: Check global builder agent status
|
|
415
|
+
if (process.argv[2] === '--agent-status') {
|
|
416
|
+
if (fs.existsSync(BUILDER_AGENT_ACTIVE)) {
|
|
417
|
+
console.log('🔧 Builder agent mode is ACTIVE');
|
|
418
|
+
} else {
|
|
419
|
+
console.log('🔒 Builder agent mode is OFF');
|
|
420
|
+
}
|
|
421
|
+
process.exit(0);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// CLI: Help
|
|
425
|
+
if (process.argv[2] === '--help' || process.argv[2] === '-h') {
|
|
426
|
+
console.log(`
|
|
427
|
+
Hailer App Edit Guard - Bulletproof protection for Hailer apps
|
|
428
|
+
|
|
429
|
+
UNIFIED SYSTEM: --agent-on/--agent-off controls BOTH this hook AND src-edit-guard.cjs.
|
|
430
|
+
One command enables builder mode for Hailer apps AND src/ directory edits.
|
|
431
|
+
|
|
432
|
+
Usage:
|
|
433
|
+
Global Builder Agent Mode (RECOMMENDED for spawning agents):
|
|
434
|
+
node app-edit-guard.cjs --agent-on Enable global builder mode
|
|
435
|
+
node app-edit-guard.cjs --agent-off Disable global builder mode
|
|
436
|
+
node app-edit-guard.cjs --agent-status Check if builder mode is active
|
|
437
|
+
|
|
438
|
+
Per-App Builder Mode:
|
|
439
|
+
node app-edit-guard.cjs --builder-on <path> Enable builder mode for an app
|
|
440
|
+
node app-edit-guard.cjs --builder-off <path> Disable builder mode for an app
|
|
441
|
+
|
|
442
|
+
Manual Editing (for direct edits by main agent):
|
|
443
|
+
node app-edit-guard.cjs --release <path> Release an app for manual editing
|
|
444
|
+
node app-edit-guard.cjs --revoke <path> Revoke manual editing permission
|
|
445
|
+
|
|
446
|
+
Utilities:
|
|
447
|
+
node app-edit-guard.cjs --check <path> Check if path is a Hailer app
|
|
448
|
+
node app-edit-guard.cjs --list List all released apps
|
|
449
|
+
node app-edit-guard.cjs --help Show this help
|
|
450
|
+
|
|
451
|
+
Workflow for spawning builder agents (RECOMMENDED):
|
|
452
|
+
1. Main agent: node app-edit-guard.cjs --agent-on
|
|
453
|
+
2. Main agent spawns builder agent via Task tool
|
|
454
|
+
3. Builder agent can freely edit ANY Hailer app AND src/ files
|
|
455
|
+
4. Main agent: node app-edit-guard.cjs --agent-off
|
|
456
|
+
|
|
457
|
+
As a hook:
|
|
458
|
+
Reads JSON from stdin with tool_name and tool_input
|
|
459
|
+
Outputs JSON with decision: "allow" or "block"
|
|
460
|
+
`);
|
|
461
|
+
process.exit(0);
|
|
462
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code UserPromptSubmit Hook - Interactive Question Mode
|
|
4
|
+
*
|
|
5
|
+
* Injects a reminder to ask clarifying questions before starting complex tasks.
|
|
6
|
+
* Triggers on every user prompt to encourage interactive behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Read hook input from stdin
|
|
10
|
+
let input = '';
|
|
11
|
+
process.stdin.setEncoding('utf8');
|
|
12
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
13
|
+
process.stdin.on('end', () => {
|
|
14
|
+
try {
|
|
15
|
+
const data = JSON.parse(input);
|
|
16
|
+
processHook(data);
|
|
17
|
+
} catch {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function processHook(data) {
|
|
23
|
+
const { prompt } = data;
|
|
24
|
+
|
|
25
|
+
if (!prompt) {
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lowerPrompt = prompt.toLowerCase();
|
|
30
|
+
|
|
31
|
+
// Detect task types that benefit from questions
|
|
32
|
+
const taskPatterns = [
|
|
33
|
+
{ pattern: /build|create|make.*app/i, type: 'app', questions: ['What data to display?', 'What layout/components?', 'What user actions needed?'] },
|
|
34
|
+
{ pattern: /create|add.*insight|report/i, type: 'insight', questions: ['What metrics/aggregations?', 'Which workflows to query?', 'Any filters needed?'] },
|
|
35
|
+
{ pattern: /import|create.*activit|bulk/i, type: 'data', questions: ['Which workflow?', 'What field values?', 'How many records?'] },
|
|
36
|
+
{ pattern: /add|create.*field|workflow|phase/i, type: 'schema', questions: ['Field type?', 'Required or optional?', 'Default values?'] },
|
|
37
|
+
{ pattern: /update|change|modify/i, type: 'update', questions: ['Which records affected?', 'What new values?', 'Confirm before applying?'] },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const matched = taskPatterns.find(p => p.pattern.test(prompt));
|
|
41
|
+
|
|
42
|
+
if (matched) {
|
|
43
|
+
const output = `
|
|
44
|
+
<interactive-mode>
|
|
45
|
+
BEFORE STARTING: Consider asking clarifying questions.
|
|
46
|
+
|
|
47
|
+
Task type detected: ${matched.type}
|
|
48
|
+
Suggested questions to ask user:
|
|
49
|
+
${matched.questions.map(q => `- ${q}`).join('\n')}
|
|
50
|
+
|
|
51
|
+
Use AskUserQuestion tool if requirements are unclear.
|
|
52
|
+
Gather specifics before spawning agents or making changes.
|
|
53
|
+
</interactive-mode>
|
|
54
|
+
`;
|
|
55
|
+
console.log(output);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Server Guard Hook
|
|
4
|
+
*
|
|
5
|
+
* PreToolUse hook that prevents Claude from starting the MCP server.
|
|
6
|
+
* When blocked, instructs Claude to provide manual instructions to the user.
|
|
7
|
+
*
|
|
8
|
+
* Blocked commands: npm run dev, npm start, tsx src/app.ts, etc.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Commands that would start the MCP server
|
|
12
|
+
const SERVER_START_PATTERNS = [
|
|
13
|
+
/npm run dev\b/,
|
|
14
|
+
/npm run start\b/,
|
|
15
|
+
/npm start\b/,
|
|
16
|
+
/tsx\s+.*src\/app\.ts/,
|
|
17
|
+
/tsx\s+watch\s+.*src\/app\.ts/,
|
|
18
|
+
/node\s+.*src\/app\.ts/,
|
|
19
|
+
/node\s+.*dist\/app\.js/,
|
|
20
|
+
/npx\s+tsx\s+.*src\/app/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Read stdin
|
|
24
|
+
async function readStdin() {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let data = '';
|
|
27
|
+
process.stdin.setEncoding('utf8');
|
|
28
|
+
process.stdin.on('data', chunk => data += chunk);
|
|
29
|
+
process.stdin.on('end', () => resolve(data));
|
|
30
|
+
setTimeout(() => resolve(data), 100);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
try {
|
|
36
|
+
const input = await readStdin();
|
|
37
|
+
|
|
38
|
+
if (!input.trim()) {
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hookData = JSON.parse(input);
|
|
43
|
+
const command = hookData.tool_input?.command || '';
|
|
44
|
+
|
|
45
|
+
// Check if this is a server start command
|
|
46
|
+
const isServerStart = SERVER_START_PATTERNS.some(pattern => pattern.test(command));
|
|
47
|
+
|
|
48
|
+
if (!isServerStart) {
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Output instructions to stderr (appears as system reminder)
|
|
53
|
+
const instructions = `============================================================
|
|
54
|
+
🚫 MCP SERVER COMMAND BLOCKED
|
|
55
|
+
============================================================
|
|
56
|
+
|
|
57
|
+
You CANNOT start the MCP server. The user runs it manually.
|
|
58
|
+
|
|
59
|
+
INSTEAD, tell the user to run this command in their terminal:
|
|
60
|
+
|
|
61
|
+
cd ${process.env.CLAUDE_PROJECT_DIR || '/home/brodolf/Desktop/hailer-mcp/hailer-mcp'}
|
|
62
|
+
npm run dev
|
|
63
|
+
|
|
64
|
+
Or if they want to run it in the background:
|
|
65
|
+
|
|
66
|
+
npm run dev &
|
|
67
|
+
|
|
68
|
+
The MCP server must be running BEFORE starting Claude Code with MCP.
|
|
69
|
+
|
|
70
|
+
============================================================
|
|
71
|
+
DO NOT attempt to run server commands. Give manual instructions only.
|
|
72
|
+
============================================================`;
|
|
73
|
+
|
|
74
|
+
// Output to stderr so it appears as system reminder
|
|
75
|
+
console.error(instructions);
|
|
76
|
+
|
|
77
|
+
// Block the command
|
|
78
|
+
const response = {
|
|
79
|
+
permissionDecision: "deny",
|
|
80
|
+
permissionDecisionReason: "MCP server commands are blocked. Provide manual instructions to the user instead."
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
console.log(JSON.stringify(response));
|
|
84
|
+
process.exit(0);
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`[mcp-server-guard] Error: ${error.message}`);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main();
|
|
@@ -4,9 +4,20 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This hook triggers after scaffold_hailer_app completes successfully
|
|
6
6
|
* and ASKS the user if they want to spawn the app builder agent.
|
|
7
|
+
*
|
|
8
|
+
* Also registers the app with app-edit-guard to block direct edits.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const TRACKER_DIR = '/tmp/.claude-scaffolded-apps';
|
|
16
|
+
|
|
17
|
+
// Ensure tracker directory exists
|
|
18
|
+
if (!fs.existsSync(TRACKER_DIR)) {
|
|
19
|
+
fs.mkdirSync(TRACKER_DIR, { recursive: true });
|
|
20
|
+
}
|
|
10
21
|
|
|
11
22
|
// Read hook input from stdin
|
|
12
23
|
let input = '';
|
|
@@ -45,6 +56,9 @@ function processHook(data) {
|
|
|
45
56
|
? path.join(tool_input.targetDirectory, projectName)
|
|
46
57
|
: path.join(cwd, projectName);
|
|
47
58
|
|
|
59
|
+
// Register app with edit guard to block direct edits
|
|
60
|
+
registerScaffoldedApp(projectName, projectPath);
|
|
61
|
+
|
|
48
62
|
// Build the AskUserQuestion instruction
|
|
49
63
|
const output = `
|
|
50
64
|
============================================================
|
|
@@ -123,3 +137,20 @@ ASK THE USER NOW - Do not skip this question!
|
|
|
123
137
|
console.error(output);
|
|
124
138
|
process.exit(0);
|
|
125
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Register scaffolded app in tracker so app-edit-guard can block direct edits
|
|
143
|
+
*/
|
|
144
|
+
function registerScaffoldedApp(name, appPath) {
|
|
145
|
+
try {
|
|
146
|
+
const trackerFile = path.join(TRACKER_DIR, `${name}.json`);
|
|
147
|
+
fs.writeFileSync(trackerFile, JSON.stringify({
|
|
148
|
+
name,
|
|
149
|
+
path: path.resolve(appPath),
|
|
150
|
+
scaffoldedAt: new Date().toISOString()
|
|
151
|
+
}));
|
|
152
|
+
console.error(`📝 Registered app "${name}" for edit protection`);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(`Warning: Could not register app for edit protection: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -18,6 +18,8 @@ const DELETE_RISK_PATTERNS = [
|
|
|
18
18
|
/npm run groups-push\b/, // Push groups - can delete groups
|
|
19
19
|
/npm run teams-push\b/, // Push teams - can delete teams
|
|
20
20
|
/npm run insights-push\b/, // Push insights - can delete insights
|
|
21
|
+
/npm run templates-sync\b/, // Sync templates - can delete templates
|
|
22
|
+
/npm run templates-push\b/, // Push templates - can modify/delete templates
|
|
21
23
|
/hailer-sdk ws-config push\b/,
|
|
22
24
|
/hailer-sdk ws-config.*sync\b/,
|
|
23
25
|
];
|