@hailer/mcp 1.0.29 → 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 -253
- 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
|
@@ -40,21 +40,47 @@
|
|
|
40
40
|
|
|
41
41
|
const fs = require('fs');
|
|
42
42
|
const path = require('path');
|
|
43
|
+
const os = require('os');
|
|
44
|
+
|
|
45
|
+
// Skip in yolo mode
|
|
46
|
+
try {
|
|
47
|
+
const statePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', 'yolo-state.json');
|
|
48
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
49
|
+
if (state.mode === 'yolo') process.exit(0);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// ENOENT is expected when not in yolo mode - only warn on unexpected errors
|
|
52
|
+
if (e.code !== 'ENOENT' && !e.message.includes('Unexpected')) {
|
|
53
|
+
console.error(`[builder-mode-manager] Warning: ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
43
56
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
57
|
+
const TEMP_DIR = os.tmpdir();
|
|
58
|
+
const BUILDER_MODE_FILE = path.join(TEMP_DIR, '.claude-builder-agent-active');
|
|
59
|
+
const AGENT_STACK_FILE = path.join(TEMP_DIR, '.claude-agent-stack.json');
|
|
60
|
+
const LOCK_FILE = AGENT_STACK_FILE + '.lock';
|
|
61
|
+
const LOCK_TIMEOUT = 5000; // 5 seconds max wait
|
|
46
62
|
|
|
47
63
|
// Agents that need builder mode (can edit code)
|
|
64
|
+
// Uses partial matching - any subagent_type containing these strings triggers builder mode
|
|
48
65
|
const CODE_EDITING_AGENTS = [
|
|
49
|
-
'giuseppe', //
|
|
50
|
-
'gunther', //
|
|
51
|
-
'general-purpose', //
|
|
52
|
-
'agent-builder', //
|
|
53
|
-
'helga', //
|
|
54
|
-
'ingrid', //
|
|
55
|
-
'ada', //
|
|
66
|
+
'giuseppe', // agent-giuseppe-app-builder
|
|
67
|
+
'gunther', // agent-gunther-mcp-tools
|
|
68
|
+
'general-purpose', // general-purpose agent
|
|
69
|
+
'agent-builder', // agent-builder-agent-creator
|
|
70
|
+
'helga', // agent-helga-workflow-config
|
|
71
|
+
'ingrid', // agent-ingrid-doc-templates
|
|
72
|
+
'ada', // agent-ada-skill-builder
|
|
73
|
+
'simple-writer', // agent-simple-writer
|
|
74
|
+
'code-simplifier', // agent-code-simplifier
|
|
75
|
+
'marco', // agent-marco-mockup-builder
|
|
56
76
|
];
|
|
57
77
|
|
|
78
|
+
// NOTE: No in-memory cache - multiple hooks may run concurrently
|
|
79
|
+
// Each hook reads fresh from file to avoid stale reads
|
|
80
|
+
|
|
81
|
+
// Staleness threshold - remove agents older than this (4 hours - long-running agents need time)
|
|
82
|
+
const AGENT_STALENESS_MS = 4 * 60 * 60 * 1000;
|
|
83
|
+
|
|
58
84
|
// Read hook input from stdin
|
|
59
85
|
let input = '';
|
|
60
86
|
process.stdin.setEncoding('utf8');
|
|
@@ -65,38 +91,128 @@ process.stdin.on('end', () => {
|
|
|
65
91
|
processHook(data);
|
|
66
92
|
} catch (e) {
|
|
67
93
|
// Invalid JSON - allow to avoid blocking
|
|
94
|
+
console.error(`[builder-mode-manager] Warning: ${e.message}`);
|
|
68
95
|
console.log(JSON.stringify({ decision: 'allow' }));
|
|
69
96
|
process.exit(0);
|
|
70
97
|
}
|
|
71
98
|
});
|
|
72
99
|
|
|
100
|
+
function acquireLock() {
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
while (Date.now() - start < LOCK_TIMEOUT) {
|
|
103
|
+
try {
|
|
104
|
+
fs.mkdirSync(LOCK_FILE);
|
|
105
|
+
return true;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
try {
|
|
108
|
+
const stat = fs.statSync(LOCK_FILE);
|
|
109
|
+
if (Date.now() - stat.mtimeMs > 10000) {
|
|
110
|
+
fs.rmdirSync(LOCK_FILE);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
} catch (e2) { /* lock was released between check */ }
|
|
114
|
+
const waitUntil = Date.now() + 50;
|
|
115
|
+
while (Date.now() < waitUntil) { /* busy wait */ }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function releaseLock() {
|
|
122
|
+
try { fs.rmdirSync(LOCK_FILE); } catch (e) { /* already released */ }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Remove stale agents from the stack (older than AGENT_STALENESS_MS)
|
|
127
|
+
* Also clears builderModeEnabledBy if that agent was removed
|
|
128
|
+
* Returns true if any agents were removed
|
|
129
|
+
*/
|
|
130
|
+
function cleanupStaleAgents(stack) {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const originalCount = stack.agents.length;
|
|
133
|
+
|
|
134
|
+
stack.agents = stack.agents.filter(agent => {
|
|
135
|
+
// Keep agents without timestamp (backward compatibility - assume current session)
|
|
136
|
+
if (!agent.startedAt) return true;
|
|
137
|
+
|
|
138
|
+
const agentTime = new Date(agent.startedAt).getTime();
|
|
139
|
+
|
|
140
|
+
// Remove agents with invalid timestamps (NaN from malformed dates)
|
|
141
|
+
if (isNaN(agentTime)) {
|
|
142
|
+
console.error(`[builder-mode-manager] Warning: Invalid timestamp for agent ${agent.type}, removing`);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (now - agentTime) < AGENT_STALENESS_MS;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const removed = originalCount - stack.agents.length;
|
|
150
|
+
if (removed > 0) {
|
|
151
|
+
console.error(`[builder-mode-manager] Cleaned up ${removed} stale agent(s) from stack`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Clear builderModeEnabledBy if that agent is no longer in stack OR stack is empty
|
|
155
|
+
if (stack.builderModeEnabledBy) {
|
|
156
|
+
const enablerStillActive = stack.agents.some(a => a.type === stack.builderModeEnabledBy);
|
|
157
|
+
if (!enablerStillActive || stack.agents.length === 0) {
|
|
158
|
+
stack.builderModeEnabledBy = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return removed > 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
73
165
|
/**
|
|
74
166
|
* Load the agent stack (tracks nested agent calls)
|
|
167
|
+
* Always reads fresh from file - no caching to avoid race conditions
|
|
168
|
+
* Automatically cleans up stale agents from previous sessions
|
|
75
169
|
*/
|
|
76
170
|
function loadAgentStack() {
|
|
77
171
|
try {
|
|
78
172
|
if (fs.existsSync(AGENT_STACK_FILE)) {
|
|
79
|
-
|
|
173
|
+
const stack = JSON.parse(fs.readFileSync(AGENT_STACK_FILE, 'utf8'));
|
|
174
|
+
|
|
175
|
+
// Clean up stale agents from previous sessions
|
|
176
|
+
const hadStaleAgents = cleanupStaleAgents(stack);
|
|
177
|
+
|
|
178
|
+
// If we cleaned up agents, save the updated stack
|
|
179
|
+
if (hadStaleAgents) {
|
|
180
|
+
// Also reset builder mode if no agents remain
|
|
181
|
+
if (stack.agents.length === 0) {
|
|
182
|
+
stack.builderModeEnabledBy = null;
|
|
183
|
+
disableBuilderMode();
|
|
184
|
+
}
|
|
185
|
+
saveAgentStack(stack);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return stack;
|
|
80
189
|
}
|
|
81
|
-
} catch {
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error(`[builder-mode-manager] Warning: ${e.message}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
82
194
|
return { agents: [], builderModeEnabledBy: null };
|
|
83
195
|
}
|
|
84
196
|
|
|
85
197
|
/**
|
|
86
|
-
* Save the agent stack
|
|
198
|
+
* Save the agent stack atomically (write to temp, then rename)
|
|
87
199
|
*/
|
|
88
200
|
function saveAgentStack(stack) {
|
|
89
|
-
|
|
201
|
+
const tmpFile = AGENT_STACK_FILE + '.tmp-' + process.pid;
|
|
202
|
+
fs.writeFileSync(tmpFile, JSON.stringify(stack, null, 2), { mode: 0o600 });
|
|
203
|
+
fs.renameSync(tmpFile, AGENT_STACK_FILE); // Atomic on POSIX
|
|
90
204
|
}
|
|
91
205
|
|
|
92
206
|
/**
|
|
93
|
-
* Enable builder mode
|
|
207
|
+
* Enable builder mode (atomic write: temp file + rename)
|
|
94
208
|
*/
|
|
95
209
|
function enableBuilderMode(agentName) {
|
|
96
|
-
|
|
210
|
+
const tmpFile = BUILDER_MODE_FILE + '.tmp-' + process.pid;
|
|
211
|
+
fs.writeFileSync(tmpFile, JSON.stringify({
|
|
97
212
|
enabledAt: new Date().toISOString(),
|
|
98
213
|
enabledBy: agentName
|
|
99
|
-
}));
|
|
214
|
+
}), { mode: 0o600 });
|
|
215
|
+
fs.renameSync(tmpFile, BUILDER_MODE_FILE); // Atomic on POSIX
|
|
100
216
|
}
|
|
101
217
|
|
|
102
218
|
/**
|
|
@@ -118,7 +234,7 @@ function isBuilderModeActive() {
|
|
|
118
234
|
}
|
|
119
235
|
|
|
120
236
|
function processHook(data) {
|
|
121
|
-
const { tool_name, tool_input, tool_response
|
|
237
|
+
const { tool_name, tool_input, tool_response } = data;
|
|
122
238
|
|
|
123
239
|
// Only handle Task tool
|
|
124
240
|
if (tool_name !== 'Task') {
|
|
@@ -133,48 +249,61 @@ function processHook(data) {
|
|
|
133
249
|
process.exit(0);
|
|
134
250
|
}
|
|
135
251
|
|
|
136
|
-
const needsBuilderMode = CODE_EDITING_AGENTS.includes(
|
|
137
|
-
const stack = loadAgentStack();
|
|
252
|
+
const needsBuilderMode = CODE_EDITING_AGENTS.some(name => agentType.includes(name));
|
|
138
253
|
|
|
139
254
|
// Determine if this is PreToolUse or PostToolUse based on presence of tool_response
|
|
140
255
|
const isPreToolUse = tool_response === undefined;
|
|
141
256
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
257
|
+
const locked = acquireLock();
|
|
258
|
+
try {
|
|
259
|
+
const stack = loadAgentStack();
|
|
260
|
+
|
|
261
|
+
if (isPreToolUse) {
|
|
262
|
+
// === PreToolUse: Agent is about to be spawned ===
|
|
263
|
+
|
|
264
|
+
// Push agent onto stack
|
|
265
|
+
stack.agents.push({
|
|
266
|
+
type: agentType,
|
|
267
|
+
startedAt: new Date().toISOString(),
|
|
268
|
+
needsBuilderMode
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Enable builder mode if needed and not already active
|
|
272
|
+
if (needsBuilderMode && !isBuilderModeActive()) {
|
|
273
|
+
enableBuilderMode(agentType);
|
|
274
|
+
stack.builderModeEnabledBy = agentType;
|
|
275
|
+
console.error(`🔧 Builder mode AUTO-ENABLED for agent: ${agentType}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
saveAgentStack(stack);
|
|
279
|
+
|
|
280
|
+
} else {
|
|
281
|
+
// === PostToolUse: Agent has completed ===
|
|
282
|
+
|
|
283
|
+
// Remove the matching agent from stack (find last one with this type)
|
|
284
|
+
// Using findLastIndex instead of pop() to handle parallel agents completing out of order
|
|
285
|
+
const removeIdx = stack.agents.findLastIndex(a => a.type === agentType);
|
|
286
|
+
if (removeIdx >= 0) {
|
|
287
|
+
stack.agents.splice(removeIdx, 1);
|
|
288
|
+
} else {
|
|
289
|
+
// Agent not found - could be stale or already cleaned up
|
|
290
|
+
console.error(`[builder-mode-manager] Warning: agent ${agentType} not found in stack, skipping removal`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check if any remaining agents need builder mode
|
|
294
|
+
const remainingNeedBuilderMode = stack.agents.some(a => a.needsBuilderMode);
|
|
295
|
+
|
|
296
|
+
// Disable builder mode if no more agents need it
|
|
297
|
+
if (isBuilderModeActive() && !remainingNeedBuilderMode) {
|
|
298
|
+
disableBuilderMode();
|
|
299
|
+
stack.builderModeEnabledBy = null;
|
|
300
|
+
console.error(`🔒 Builder mode AUTO-DISABLED (${agentType} completed)`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
saveAgentStack(stack);
|
|
175
304
|
}
|
|
176
|
-
|
|
177
|
-
|
|
305
|
+
} finally {
|
|
306
|
+
if (locked) releaseLock();
|
|
178
307
|
}
|
|
179
308
|
|
|
180
309
|
// Always allow Task tool
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* <hook-name>bulk-activity-guard</hook-name>
|
|
4
|
+
*
|
|
5
|
+
* <purpose>
|
|
6
|
+
* Confirms before creating many activities at once.
|
|
7
|
+
* Prevents accidental bulk data creation via Dmitri agent.
|
|
8
|
+
* </purpose>
|
|
9
|
+
*
|
|
10
|
+
* <triggers>
|
|
11
|
+
* - PreToolUse on mcp__hailer__create_activity
|
|
12
|
+
* - PreToolUse on mcp__hailer__bulk_create_activities (if exists)
|
|
13
|
+
* </triggers>
|
|
14
|
+
*
|
|
15
|
+
* <thresholds>
|
|
16
|
+
* - Single call with array > 5 items: Warn
|
|
17
|
+
* - Tracked calls in short window > 10: Warn
|
|
18
|
+
* </thresholds>
|
|
19
|
+
*
|
|
20
|
+
* <behavior>
|
|
21
|
+
* 1. Check if bulk creation (array input or rapid sequential calls)
|
|
22
|
+
* 2. If threshold exceeded, block and require confirmation
|
|
23
|
+
* 3. Track recent calls in temp file for sequential detection
|
|
24
|
+
* </behavior>
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
|
|
31
|
+
// Skip in yolo mode
|
|
32
|
+
try {
|
|
33
|
+
const statePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', 'yolo-state.json');
|
|
34
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
35
|
+
if (state.mode === 'yolo') process.exit(0);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// ENOENT is expected when not in yolo mode - only warn on unexpected errors
|
|
38
|
+
if (e.code !== 'ENOENT' && !e.message.includes('Unexpected')) {
|
|
39
|
+
console.error(`[bulk-activity-guard] Warning: ${e.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Skip in subagent context - orchestrator handles confirmation via delegation-bulk-guard
|
|
44
|
+
// Subagents can't use AskUserQuestion or Bash, so blocking them is unrecoverable
|
|
45
|
+
if (process.env.CLAUDE_AGENT_ID || process.env.CLAUDE_SUBAGENT) {
|
|
46
|
+
// Output allow and exit - orchestrator already confirmed
|
|
47
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const TRACKER_FILE = path.join(os.tmpdir(), '.claude-activity-creation-tracker.json');
|
|
52
|
+
const LOCK_FILE = TRACKER_FILE + '.lock';
|
|
53
|
+
const LOCK_TIMEOUT = 5000; // 5 seconds max wait
|
|
54
|
+
const BULK_THRESHOLD = 5;
|
|
55
|
+
const SEQUENTIAL_THRESHOLD = 10;
|
|
56
|
+
const TIME_WINDOW_MS = 60000; // 1 minute window for sequential detection
|
|
57
|
+
|
|
58
|
+
// Read hook input from stdin
|
|
59
|
+
let input = '';
|
|
60
|
+
process.stdin.setEncoding('utf8');
|
|
61
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
62
|
+
process.stdin.on('end', () => {
|
|
63
|
+
try {
|
|
64
|
+
const data = JSON.parse(input);
|
|
65
|
+
processHook(data);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error(`[bulk-activity-guard] Warning: ${e.message}`);
|
|
68
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function outputAllow() {
|
|
74
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function outputBlock(message) {
|
|
79
|
+
console.log(JSON.stringify({
|
|
80
|
+
decision: 'block',
|
|
81
|
+
reason: message
|
|
82
|
+
}));
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function acquireLock() {
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
while (Date.now() - start < LOCK_TIMEOUT) {
|
|
89
|
+
try {
|
|
90
|
+
fs.mkdirSync(LOCK_FILE);
|
|
91
|
+
return true;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Lock exists - check if stale (older than 10 seconds)
|
|
94
|
+
try {
|
|
95
|
+
const stat = fs.statSync(LOCK_FILE);
|
|
96
|
+
if (Date.now() - stat.mtimeMs > 10000) {
|
|
97
|
+
fs.rmdirSync(LOCK_FILE);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
} catch (e2) { /* lock was released between check */ }
|
|
101
|
+
// Wait 50ms and retry
|
|
102
|
+
const waitUntil = Date.now() + 50;
|
|
103
|
+
while (Date.now() < waitUntil) { /* busy wait - hooks are sync */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false; // Timeout - proceed without lock (fallback)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function releaseLock() {
|
|
110
|
+
try { fs.rmdirSync(LOCK_FILE); } catch (e) { /* already released */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function loadTracker() {
|
|
114
|
+
try {
|
|
115
|
+
if (fs.existsSync(TRACKER_FILE)) {
|
|
116
|
+
return JSON.parse(fs.readFileSync(TRACKER_FILE, 'utf8'));
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error(`[bulk-activity-guard] Warning: ${e.message}`);
|
|
120
|
+
}
|
|
121
|
+
return { calls: [], confirmed: false, confirmedAt: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function saveTracker(tracker) {
|
|
125
|
+
// Atomic write: write to temp file, then rename
|
|
126
|
+
const tmpFile = TRACKER_FILE + '.tmp-' + process.pid;
|
|
127
|
+
fs.writeFileSync(tmpFile, JSON.stringify(tracker, null, 2), { mode: 0o600 });
|
|
128
|
+
fs.renameSync(tmpFile, TRACKER_FILE); // Atomic on POSIX
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function processHook(data) {
|
|
132
|
+
const { tool_name, tool_input } = data;
|
|
133
|
+
|
|
134
|
+
// Only check activity creation tools - exact match to avoid false positives
|
|
135
|
+
if (!tool_name || (tool_name !== 'mcp__hailer__create_activity' && tool_name !== 'mcp__hailer__bulk_create_activities')) {
|
|
136
|
+
outputAllow();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const locked = acquireLock();
|
|
141
|
+
try {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const tracker = loadTracker();
|
|
144
|
+
|
|
145
|
+
// Clean old calls outside time window
|
|
146
|
+
tracker.calls = tracker.calls.filter(t => now - t < TIME_WINDOW_MS);
|
|
147
|
+
|
|
148
|
+
// Check if recently confirmed (within last 5 minutes)
|
|
149
|
+
if (tracker.confirmed && tracker.confirmedAt && (now - tracker.confirmedAt) < 300000) {
|
|
150
|
+
// User recently confirmed bulk creation, allow
|
|
151
|
+
tracker.calls.push(now);
|
|
152
|
+
saveTracker(tracker);
|
|
153
|
+
outputAllow();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Reset confirmation if window expired
|
|
158
|
+
tracker.confirmed = false;
|
|
159
|
+
tracker.confirmedAt = null;
|
|
160
|
+
|
|
161
|
+
// Check for bulk array in input
|
|
162
|
+
let itemCount = 1;
|
|
163
|
+
try {
|
|
164
|
+
if (tool_input?.activities && Array.isArray(tool_input.activities)) {
|
|
165
|
+
itemCount = tool_input.activities.length;
|
|
166
|
+
}
|
|
167
|
+
} catch {}
|
|
168
|
+
|
|
169
|
+
// Add current call
|
|
170
|
+
tracker.calls.push(now);
|
|
171
|
+
const recentCallCount = tracker.calls.length;
|
|
172
|
+
saveTracker(tracker);
|
|
173
|
+
|
|
174
|
+
// Check thresholds
|
|
175
|
+
const isBulkArray = itemCount > BULK_THRESHOLD;
|
|
176
|
+
const isSequentialBulk = recentCallCount > SEQUENTIAL_THRESHOLD;
|
|
177
|
+
|
|
178
|
+
if (isBulkArray || isSequentialBulk) {
|
|
179
|
+
const reason = isBulkArray
|
|
180
|
+
? `Creating ${itemCount} activities in one call`
|
|
181
|
+
: `${recentCallCount} activity creations in the last minute`;
|
|
182
|
+
|
|
183
|
+
outputBlock(`
|
|
184
|
+
🚫 BLOCKED: Bulk Activity Creation Detected
|
|
185
|
+
|
|
186
|
+
**Reason:** ${reason}
|
|
187
|
+
|
|
188
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
189
|
+
CONFIRM WITH USER FIRST
|
|
190
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
191
|
+
|
|
192
|
+
Use AskUserQuestion to confirm:
|
|
193
|
+
|
|
194
|
+
\`\`\`json
|
|
195
|
+
{
|
|
196
|
+
"questions": [{
|
|
197
|
+
"question": "About to create ${isBulkArray ? itemCount : recentCallCount}+ activities. Continue?",
|
|
198
|
+
"header": "Bulk Create",
|
|
199
|
+
"options": [
|
|
200
|
+
{ "label": "Yes, create all", "description": "Proceed with bulk creation" },
|
|
201
|
+
{ "label": "No, stop", "description": "Cancel the operation" }
|
|
202
|
+
],
|
|
203
|
+
"multiSelect": false
|
|
204
|
+
}]
|
|
205
|
+
}
|
|
206
|
+
\`\`\`
|
|
207
|
+
|
|
208
|
+
If user confirms, run this to unlock for 5 minutes:
|
|
209
|
+
Bash: node "${process.argv[1].replace(/'/g, "'\\''")}" --confirm
|
|
210
|
+
|
|
211
|
+
Then retry the operation.
|
|
212
|
+
|
|
213
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
214
|
+
`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
outputAllow();
|
|
218
|
+
} finally {
|
|
219
|
+
if (locked) releaseLock();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// CLI: Confirm bulk creation
|
|
224
|
+
if (process.argv[2] === '--confirm') {
|
|
225
|
+
const locked = acquireLock();
|
|
226
|
+
try {
|
|
227
|
+
const tracker = loadTracker();
|
|
228
|
+
tracker.confirmed = true;
|
|
229
|
+
tracker.confirmedAt = Date.now();
|
|
230
|
+
saveTracker(tracker);
|
|
231
|
+
console.log('✅ Bulk activity creation confirmed for 5 minutes');
|
|
232
|
+
} finally {
|
|
233
|
+
if (locked) releaseLock();
|
|
234
|
+
}
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// CLI: Reset tracker
|
|
239
|
+
if (process.argv[2] === '--reset') {
|
|
240
|
+
if (fs.existsSync(TRACKER_FILE)) {
|
|
241
|
+
fs.unlinkSync(TRACKER_FILE);
|
|
242
|
+
}
|
|
243
|
+
console.log('✅ Activity creation tracker reset');
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// CLI: Status
|
|
248
|
+
if (process.argv[2] === '--status') {
|
|
249
|
+
const tracker = loadTracker();
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
const recentCalls = tracker.calls.filter(t => now - t < TIME_WINDOW_MS).length;
|
|
252
|
+
console.log('Activity Creation Tracker');
|
|
253
|
+
console.log('=========================');
|
|
254
|
+
console.log(`Recent calls (1 min): ${recentCalls}`);
|
|
255
|
+
console.log(`Confirmed: ${tracker.confirmed}`);
|
|
256
|
+
if (tracker.confirmedAt) {
|
|
257
|
+
const remaining = Math.max(0, 300000 - (now - tracker.confirmedAt));
|
|
258
|
+
console.log(`Confirmation expires in: ${Math.round(remaining / 1000)}s`);
|
|
259
|
+
}
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// CLI: Help
|
|
264
|
+
if (process.argv[2] === '--help' || process.argv[2] === '-h') {
|
|
265
|
+
console.log(`
|
|
266
|
+
Bulk Activity Guard - Confirms before bulk activity creation
|
|
267
|
+
|
|
268
|
+
Usage:
|
|
269
|
+
node bulk-activity-guard.cjs --confirm Confirm bulk creation for 5 minutes
|
|
270
|
+
node bulk-activity-guard.cjs --reset Reset the tracker
|
|
271
|
+
node bulk-activity-guard.cjs --status Show current status
|
|
272
|
+
node bulk-activity-guard.cjs --help Show this help
|
|
273
|
+
|
|
274
|
+
Thresholds:
|
|
275
|
+
- Single call with >${BULK_THRESHOLD} items: Blocked
|
|
276
|
+
- >${SEQUENTIAL_THRESHOLD} calls in 1 minute: Blocked
|
|
277
|
+
|
|
278
|
+
As a hook:
|
|
279
|
+
Reads JSON from stdin with tool_name and tool_input
|
|
280
|
+
Outputs JSON with decision: "allow" or "block"
|
|
281
|
+
`);
|
|
282
|
+
process.exit(0);
|
|
283
|
+
}
|