@hailer/mcp 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude/agents/ada.md +71 -112
  2. package/.claude/agents/agent-builder.md +69 -136
  3. package/.claude/agents/alejandro.md +38 -52
  4. package/.claude/agents/bjorn.md +49 -300
  5. package/.claude/agents/dmitri.md +31 -50
  6. package/.claude/agents/giuseppe.md +45 -54
  7. package/.claude/agents/gunther.md +63 -350
  8. package/.claude/agents/helga.md +48 -91
  9. package/.claude/agents/ingrid.md +48 -99
  10. package/.claude/agents/kenji.md +44 -52
  11. package/.claude/agents/svetlana.md +53 -389
  12. package/.claude/agents/viktor.md +41 -51
  13. package/.claude/agents/yevgeni.md +27 -48
  14. package/.claude/assistant-knowledge.md +23 -0
  15. package/.claude/hooks/agent-failure-detector.cjs +27 -3
  16. package/.claude/hooks/app-edit-guard.cjs +33 -10
  17. package/.claude/hooks/builder-mode-manager.cjs +237 -0
  18. package/.claude/hooks/interactive-mode.cjs +29 -3
  19. package/.claude/hooks/mcp-server-guard.cjs +20 -4
  20. package/.claude/hooks/post-scaffold-hook.cjs +23 -4
  21. package/.claude/hooks/publish-template-guard.cjs +29 -11
  22. package/.claude/hooks/src-edit-guard.cjs +30 -6
  23. package/.claude/settings.json +13 -3
  24. package/.claude/skills/insight-join-patterns/SKILL.md +50 -88
  25. package/.claude/skills/json-only-output/SKILL.md +32 -0
  26. package/.claude/skills/optional-parameters/SKILL.md +63 -0
  27. package/.claude/skills/tool-response-verification/SKILL.md +58 -0
  28. package/CLAUDE.md +114 -111
  29. package/dist/client/mcp-assistant.d.ts +21 -0
  30. package/dist/client/mcp-assistant.js +58 -0
  31. package/dist/client/mcp-client.js +8 -2
  32. package/dist/client/providers/assistant-provider.d.ts +17 -0
  33. package/dist/client/providers/assistant-provider.js +51 -0
  34. package/dist/client/providers/openai-provider.js +10 -1
  35. package/dist/client/tool-schema-loader.d.ts +1 -0
  36. package/dist/client/tool-schema-loader.js +9 -1
  37. package/dist/client/types.d.ts +2 -1
  38. package/dist/config.d.ts +5 -1
  39. package/dist/config.js +11 -5
  40. package/mcp-system-prompt.txt +127 -0
  41. package/package.json +1 -1
  42. package/.claude/hooks/sdk-delete-guard.cjs +0 -119
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * MCP Assistant (Lightweight)
4
+ *
5
+ * Pure pass-through chatbot. System prompt is loaded on AI PC side.
6
+ * Just forwards user messages, no local processing.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.McpAssistant = void 0;
13
+ const openai_1 = __importDefault(require("openai"));
14
+ const logger_1 = require("../lib/logger");
15
+ const logger = (0, logger_1.createLogger)({ component: 'McpAssistant' });
16
+ class McpAssistant {
17
+ client;
18
+ constructor(config) {
19
+ this.client = new openai_1.default({
20
+ apiKey: config.apiKey,
21
+ baseURL: config.baseURL,
22
+ });
23
+ }
24
+ /**
25
+ * Chat - pure pass-through to local LLM
26
+ */
27
+ async chat(userMessage, conversationHistory = []) {
28
+ const model = process.env.OPENAI_MODEL || 'gpt-4o';
29
+ try {
30
+ const messages = [
31
+ ...conversationHistory.slice(-4).map(msg => ({
32
+ role: msg.role,
33
+ content: msg.content,
34
+ })),
35
+ { role: 'user', content: userMessage },
36
+ ];
37
+ logger.debug('Chat', { model, msgCount: messages.length });
38
+ const response = await this.client.chat.completions.create({
39
+ model,
40
+ messages,
41
+ max_tokens: 500,
42
+ temperature: 0.7,
43
+ });
44
+ const reply = response.choices[0]?.message?.content || 'Could not respond.';
45
+ logger.info('Response', {
46
+ in: response.usage?.prompt_tokens,
47
+ out: response.usage?.completion_tokens,
48
+ });
49
+ return reply;
50
+ }
51
+ catch (error) {
52
+ logger.error('Chat failed', error);
53
+ throw error;
54
+ }
55
+ }
56
+ }
57
+ exports.McpAssistant = McpAssistant;
58
+ //# sourceMappingURL=mcp-assistant.js.map
@@ -8,6 +8,7 @@ exports.McpClient = void 0;
8
8
  const message_processor_1 = require("./message-processor");
9
9
  const openai_provider_1 = require("./providers/openai-provider");
10
10
  const anthropic_provider_1 = require("./providers/anthropic-provider");
11
+ const assistant_provider_1 = require("./providers/assistant-provider");
11
12
  const multi_bot_manager_1 = require("./multi-bot-manager");
12
13
  const agent_tracker_1 = require("./agent-tracker");
13
14
  const token_tracker_1 = require("./token-tracker");
@@ -102,6 +103,9 @@ class McpClient {
102
103
  case "anthropic":
103
104
  provider = new anthropic_provider_1.AnthropicProvider(providerConfig);
104
105
  break;
106
+ case "assistant":
107
+ provider = new assistant_provider_1.AssistantProvider(providerConfig);
108
+ break;
105
109
  case "gemini":
106
110
  // TODO: Implement GeminiProvider
107
111
  console.warn(`⚠️ Provider type ${providerConfig.type} not yet implemented`);
@@ -306,9 +310,11 @@ class McpClient {
306
310
  }
307
311
  return; // Skip LLM processing
308
312
  }
309
- // Generate and post personalized confirmation message
313
+ // Generate and post personalized confirmation message (skip if empty)
310
314
  const confirmationMessage = await provider.generateConfirmationMessage(message);
311
- await this.messageProcessor.postMessage(message.discussionId, confirmationMessage, message.workspaceId, message.mentionedOrDirectMessagedBotId);
315
+ if (confirmationMessage) {
316
+ await this.messageProcessor.postMessage(message.discussionId, confirmationMessage, message.workspaceId, message.mentionedOrDirectMessagedBotId);
317
+ }
312
318
  const response = await this.processMessage(message, provider);
313
319
  await this.handleResponse(message, response);
314
320
  // LOG COMPLETION EVENT - Bot finished processing
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Assistant Provider
3
+ *
4
+ * Pure pass-through chatbot. No tools, no local system prompt.
5
+ * System prompt is loaded on AI PC side via llama-server.
6
+ */
7
+ import { ChatMessage, McpResponse } from "../types";
8
+ import { LlmProvider } from "./llm-provider";
9
+ import { LlmProviderConfig } from "../types";
10
+ export declare class AssistantProvider extends LlmProvider {
11
+ private assistant;
12
+ constructor(config: LlmProviderConfig);
13
+ generateConfirmationMessage(_userMessage: ChatMessage): Promise<string>;
14
+ processMessage(userMessage: ChatMessage, _mcpServerUrl: string, _botMcpApiKey: string, _botEmail: string): Promise<McpResponse>;
15
+ protected callMcpTool(): Promise<any>;
16
+ }
17
+ //# sourceMappingURL=assistant-provider.d.ts.map
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Assistant Provider
4
+ *
5
+ * Pure pass-through chatbot. No tools, no local system prompt.
6
+ * System prompt is loaded on AI PC side via llama-server.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.AssistantProvider = void 0;
10
+ const llm_provider_1 = require("./llm-provider");
11
+ const mcp_assistant_1 = require("../mcp-assistant");
12
+ class AssistantProvider extends llm_provider_1.LlmProvider {
13
+ assistant;
14
+ constructor(config) {
15
+ super(config);
16
+ this.assistant = new mcp_assistant_1.McpAssistant({
17
+ apiKey: config.apiKey,
18
+ baseURL: config.baseURL,
19
+ });
20
+ }
21
+ async generateConfirmationMessage(_userMessage) {
22
+ // No confirmation message - direct response only
23
+ return '';
24
+ }
25
+ async processMessage(userMessage, _mcpServerUrl, _botMcpApiKey, _botEmail) {
26
+ if (!this.isEnabled()) {
27
+ return { success: false, error: "Assistant not enabled" };
28
+ }
29
+ try {
30
+ const startTime = Date.now();
31
+ const cleanedMessage = this.removeMentions(userMessage);
32
+ this.logger.debug('Chat request', { user: userMessage.userName });
33
+ const response = await this.assistant.chat(cleanedMessage);
34
+ this.logger.info('Chat done', { duration: Date.now() - startTime });
35
+ return {
36
+ success: true,
37
+ response,
38
+ toolCalls: [],
39
+ };
40
+ }
41
+ catch (error) {
42
+ this.logError(error, "processMessage");
43
+ return { success: false, error: error.message };
44
+ }
45
+ }
46
+ async callMcpTool() {
47
+ throw new Error("Assistant does not use tools");
48
+ }
49
+ }
50
+ exports.AssistantProvider = AssistantProvider;
51
+ //# sourceMappingURL=assistant-provider.js.map
@@ -20,6 +20,7 @@ class OpenAIProvider extends llm_provider_1.LlmProvider {
20
20
  super(config);
21
21
  this.client = new openai_1.default({
22
22
  apiKey: config.apiKey,
23
+ baseURL: config.baseURL,
23
24
  });
24
25
  this.contextManager = (0, context_manager_1.getContextManager)({
25
26
  openaiApiKey: config.apiKey,
@@ -72,11 +73,19 @@ class OpenAIProvider extends llm_provider_1.LlmProvider {
72
73
  const startTime = Date.now();
73
74
  // Load tool index with automatic filtering
74
75
  // Chat bot only gets READ + WRITE tools (no PLAYGROUND tools)
76
+ // In 'minimal' mode, only load essential chat tools (~3 tools vs 40+)
75
77
  const allowedGroups = [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE];
78
+ const minimalChatTools = [
79
+ 'add_discussion_message',
80
+ 'fetch_discussion_messages',
81
+ 'search_workspace_users'
82
+ ];
83
+ const isMinimalMode = process.env.CHAT_BOT_MODE === 'minimal';
76
84
  const toolIndex = await this.toolSchemaLoader.loadToolIndex({
77
85
  mcpServerUrl,
78
86
  mcpServerApiKey: botMcpApiKey,
79
- allowedGroups
87
+ allowedGroups,
88
+ allowedTools: isMinimalMode ? minimalChatTools : undefined
80
89
  });
81
90
  if (!toolIndex || toolIndex.length === 0) {
82
91
  this.logger.warn("No MCP tools available");
@@ -25,6 +25,7 @@ export interface LoadToolsOptions {
25
25
  mcpServerUrl: string;
26
26
  mcpServerApiKey: string;
27
27
  allowedGroups: ToolGroup[];
28
+ allowedTools?: string[];
28
29
  excludeMessageFetchTools?: boolean;
29
30
  }
30
31
  export declare class ToolSchemaLoader {
@@ -22,9 +22,17 @@ class ToolSchemaLoader {
22
22
  * Returns lightweight tool list with optional exclusions
23
23
  */
24
24
  async loadToolIndex(options) {
25
- const { mcpServerUrl, mcpServerApiKey, allowedGroups, excludeMessageFetchTools } = options;
25
+ const { mcpServerUrl, mcpServerApiKey, allowedGroups, allowedTools, excludeMessageFetchTools } = options;
26
26
  // Fetch tool index from MCP server with group filtering
27
27
  let toolIndex = await this.fetchMcpToolIndex(mcpServerUrl, mcpServerApiKey, allowedGroups);
28
+ // Filter by explicit whitelist if provided (overrides group filtering)
29
+ if (allowedTools && allowedTools.length > 0) {
30
+ toolIndex = toolIndex.filter(tool => allowedTools.includes(tool.name));
31
+ logger.info("Filtered tools by whitelist", {
32
+ allowedTools,
33
+ resultCount: toolIndex.length
34
+ });
35
+ }
28
36
  // Optionally exclude message fetch tools if explicitly requested
29
37
  if (excludeMessageFetchTools) {
30
38
  const excludedTools = ['fetch_discussion_messages', 'fetch_previous_discussion_messages'];
@@ -16,8 +16,9 @@ export interface McpClientConfig {
16
16
  }
17
17
  export interface LlmProviderConfig {
18
18
  name: string;
19
- type: "openai" | "anthropic" | "gemini";
19
+ type: "openai" | "anthropic" | "gemini" | "assistant";
20
20
  apiKey: string;
21
+ baseURL?: string;
21
22
  model: string;
22
23
  enabled: boolean;
23
24
  maxTokens?: number;
package/dist/config.d.ts CHANGED
@@ -35,6 +35,7 @@ export declare const environment: {
35
35
  MCP_CLIENT_AGENT_IDS: string[];
36
36
  TOKEN_USAGE_BOT_ENABLED: boolean;
37
37
  AGENT_ACTIVITY_BOT_ENABLED: boolean;
38
+ CHAT_BOT_MODE: "full" | "minimal" | "assistant";
38
39
  ADAPTIVE_DOCUMENTATION_BOT_ENABLED: boolean;
39
40
  ADAPTIVE_AUTO_UPDATE: boolean;
40
41
  ADAPTIVE_UPDATE_INTERVAL: number;
@@ -49,6 +50,8 @@ export declare const environment: {
49
50
  WORKSPACE_CONFIG_PATH?: string | undefined;
50
51
  DEV_APPS_PATH?: string | undefined;
51
52
  OPENAI_API_KEY?: string | undefined;
53
+ OPENAI_API_BASE?: string | undefined;
54
+ OPENAI_MODEL?: string | undefined;
52
55
  ANTHROPIC_API_KEY?: string | undefined;
53
56
  };
54
57
  export interface HailerAccount {
@@ -60,8 +63,9 @@ export interface HailerAccount {
60
63
  }
61
64
  export interface LlmProvider {
62
65
  name: string;
63
- type: 'openai' | 'anthropic';
66
+ type: 'openai' | 'anthropic' | 'assistant';
64
67
  apiKey: string;
68
+ baseURL?: string;
65
69
  model: string;
66
70
  enabled: boolean;
67
71
  }
package/dist/config.js CHANGED
@@ -102,6 +102,8 @@ const environmentSchema = zod_1.z.object({
102
102
  DEV_APPS_PATH: zod_1.z.string().optional(),
103
103
  // LLM providers
104
104
  OPENAI_API_KEY: zod_1.z.string().min(1).optional(),
105
+ OPENAI_API_BASE: zod_1.z.string().url().optional(),
106
+ OPENAI_MODEL: zod_1.z.string().optional(),
105
107
  ANTHROPIC_API_KEY: zod_1.z.string().min(1).optional(),
106
108
  // MCP client settings
107
109
  MCP_SERVER_URL: zod_1.z.string().url().default('http://localhost:3030/api/mcp'),
@@ -110,6 +112,8 @@ const environmentSchema = zod_1.z.object({
110
112
  // Bot features
111
113
  TOKEN_USAGE_BOT_ENABLED: zod_1.z.string().transform(v => v !== 'false').default('true'),
112
114
  AGENT_ACTIVITY_BOT_ENABLED: zod_1.z.string().transform(v => v !== 'false').default('true'),
115
+ // Chat bot mode: 'full' = all tools, 'minimal' = chat tools only, 'assistant' = no tools (knowledge-based help)
116
+ CHAT_BOT_MODE: zod_1.z.enum(['full', 'minimal', 'assistant']).default('full'),
113
117
  ADAPTIVE_DOCUMENTATION_BOT_ENABLED: zod_1.z.string().transform(v => v === 'true').default('false'),
114
118
  ADAPTIVE_AUTO_UPDATE: zod_1.z.string().transform(v => v === 'true').default('false'),
115
119
  ADAPTIVE_UPDATE_INTERVAL: zod_1.z.string().transform(v => parseInt(v) || 60000).default('60000'),
@@ -164,12 +168,14 @@ class ApplicationConfig {
164
168
  }
165
169
  get llmProviders() {
166
170
  const providers = [];
167
- if (exports.environment.OPENAI_API_KEY) {
171
+ if (exports.environment.OPENAI_API_KEY || exports.environment.OPENAI_API_BASE) {
172
+ const isAssistantMode = exports.environment.CHAT_BOT_MODE === 'assistant';
168
173
  providers.push({
169
- name: 'openai-gpt4o',
170
- type: 'openai',
171
- apiKey: exports.environment.OPENAI_API_KEY,
172
- model: 'gpt-4o',
174
+ name: isAssistantMode ? 'mcp-assistant' : (exports.environment.OPENAI_API_BASE ? 'local-llm' : 'openai-gpt4o'),
175
+ type: isAssistantMode ? 'assistant' : 'openai',
176
+ apiKey: exports.environment.OPENAI_API_KEY || 'not-needed',
177
+ baseURL: exports.environment.OPENAI_API_BASE,
178
+ model: exports.environment.OPENAI_MODEL || 'gpt-4o',
173
179
  enabled: true,
174
180
  });
175
181
  }
@@ -0,0 +1,127 @@
1
+ You are the MCP Assistant for Hailer MCP Server. Help users understand what MCP agents can do and guide them to the right agent for their task.
2
+
3
+ # What is Hailer MCP?
4
+
5
+ Hailer MCP is a Model Context Protocol server that connects Claude Code to Hailer workspaces. It provides 34+ tools for managing workflows, activities, insights, apps, and discussions.
6
+
7
+ Hailer is a SaaS platform for business process management with:
8
+ - Workspaces (organizations)
9
+ - Workflows (kanban-style process boards with phases)
10
+ - Activities (data records within workflows)
11
+ - Discussions (chat threads attached to activities)
12
+ - Insights (SQL-like reports over workflow data)
13
+ - Apps (custom React applications)
14
+
15
+ # Available Agents
16
+
17
+ ## Fast Agents (haiku model)
18
+
19
+ ### kenji - Data Reader
20
+ - Reads schema, fields, phases from local workspace/ files
21
+ - Falls back to API for activity data and counts
22
+ - Use for: "What fields does X have?", "List workflows", "Show phases"
23
+
24
+ ### dmitri - Data Writer
25
+ - Creates and updates activities (single or bulk)
26
+ - Use for: "Create customer", "Update 50 records", "Move to phase"
27
+ - Needs: workflow_id, phase_id, field values from kenji first
28
+
29
+ ### yevgeni - Discussion Handler
30
+ - Reads/posts messages, manages discussion membership
31
+ - Use for: "Post message", "Read chat history", "Invite user to discussion"
32
+
33
+ ### bjorn - Config Auditor
34
+ - Audits CLAUDE.md, agents, hooks, settings.json
35
+ - Use for: "Check if config is correct", "Find orphaned files"
36
+
37
+ ## Reasoning Agents (sonnet model)
38
+
39
+ ### helga - Workspace Config
40
+ - Manages workflows, fields, phases via TypeScript SDK files
41
+ - Use for: "Add field to workflow", "Create new workflow", "Rename phase"
42
+ - Returns commands for orchestrator to run (npm run push)
43
+
44
+ ### viktor - SQL Insights
45
+ - Creates SQL-like reports over workflow data
46
+ - Use for: "Report of high priority tasks", "Sales by month", "Join customers with orders"
47
+ - Always previews before creating
48
+
49
+ ### giuseppe - App Builder
50
+ - Builds Hailer apps with React/TypeScript/Chakra
51
+ - Use for: "Build dashboard showing customers", "Create kanban app"
52
+ - Needs: workflow_id, phase_id, field_ids from kenji first
53
+
54
+ ### alejandro - Function Fields
55
+ - Creates calculated/formula fields in workflows
56
+ - Use for: "Add Total Cost = qty * price", "Concatenate first + last name"
57
+ - Always tests formulas before enabling
58
+
59
+ ### gunther - MCP Tool Builder
60
+ - Creates new MCP tools for the server itself
61
+ - Use for: "Add tool to count activities", "Create new API endpoint"
62
+
63
+ ### svetlana - Code Reviewer
64
+ - Reviews code for bugs, security, best practices (READ-ONLY)
65
+ - Use for: "Review my changes", "Check for security issues"
66
+
67
+ ### ada - Skill Creator
68
+ - Creates skills when agents fail repeatedly
69
+ - Use for: "Viktor keeps getting JOINs wrong", "Document this pattern"
70
+
71
+ ### ingrid - Document Templates
72
+ - Creates PDF/CSV document templates
73
+ - Use for: "Create invoice template", "Add report template"
74
+
75
+ ### agent-builder - Agent Creator
76
+ - Creates new lean agents (<50 lines)
77
+ - Use for: "I need an agent for X"
78
+
79
+ # How Agents Work
80
+
81
+ 1. User asks the orchestrator (main Claude)
82
+ 2. Orchestrator delegates to appropriate agent
83
+ 3. Agent returns JSON with result
84
+ 4. Orchestrator interprets and reports back
85
+
86
+ Agents output JSON only:
87
+ { "status": "success|error", "result": {...}, "summary": "..." }
88
+
89
+ # Common Workflows
90
+
91
+ ## Read data
92
+ kenji → list workflows, get schema, get fields
93
+
94
+ ## Create/update data
95
+ kenji (get IDs) → dmitri (create/update)
96
+
97
+ ## Build report
98
+ kenji (get schema) → viktor (create insight)
99
+
100
+ ## Build app
101
+ kenji (get IDs) → giuseppe (scaffold + build)
102
+
103
+ ## Configure workspace
104
+ helga (edit files) → orchestrator runs push commands
105
+
106
+ # Current Version: 0.0.5
107
+
108
+ Recent changes:
109
+ - Hailer app builder improvements
110
+ - Marketplace template publishing
111
+ - Safety hooks for destructive operations
112
+ - 34+ MCP tools available
113
+
114
+ # Limitations
115
+
116
+ - Agents run via Claude Code CLI, not standalone
117
+ - Workspace-scoped (cannot access external systems)
118
+ - Orchestrator must provide IDs to write agents
119
+ - Some operations require npm run commands
120
+
121
+ # Guidelines
122
+
123
+ - Be concise (1-3 sentences per response)
124
+ - Match user's language
125
+ - If unsure which agent, ask clarifying question
126
+ - You guide users, you cannot execute actions yourself
127
+ - Recommend kenji first for any data lookup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hailer/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "config": {
5
5
  "docker": {
6
6
  "registry": "registry.gitlab.com/hailer-repos/hailer-mcp"
@@ -1,119 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * SDK Delete Guard Hook
4
- *
5
- * PreToolUse hook that catches SDK commands which may delete resources
6
- * and blocks them, instructing Claude to use `yes |` prefix if user confirms.
7
- *
8
- * Triggered by: npm run push, npm run *-sync, npm run *-push commands
9
- */
10
-
11
- // Commands that can cause deletions
12
- const DELETE_RISK_PATTERNS = [
13
- /npm run push\b/, // Full push - can delete workflows, fields, phases
14
- /npm run workflows-sync\b/, // Sync workflows - can delete workflows
15
- /npm run workflows-push\b/, // Push workflows - can delete workflows
16
- /npm run phases-push\b/, // Push phases - can delete phases
17
- /npm run fields-push\b/, // Push fields - can delete fields
18
- /npm run groups-push\b/, // Push groups - can delete groups
19
- /npm run teams-push\b/, // Push teams - can delete teams
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
23
- /hailer-sdk ws-config push\b/,
24
- /hailer-sdk ws-config.*sync\b/,
25
- ];
26
-
27
- // Commands that are already safe (have yes | prefix)
28
- const SAFE_PATTERNS = [
29
- /^yes\s*\|/,
30
- /^echo\s+[yYnN]\s*\|/,
31
- ];
32
-
33
- // Read stdin
34
- async function readStdin() {
35
- return new Promise((resolve) => {
36
- let data = '';
37
- process.stdin.setEncoding('utf8');
38
- process.stdin.on('data', chunk => data += chunk);
39
- process.stdin.on('end', () => resolve(data));
40
- setTimeout(() => resolve(data), 100);
41
- });
42
- }
43
-
44
- async function main() {
45
- try {
46
- const input = await readStdin();
47
-
48
- if (!input.trim()) {
49
- process.exit(0);
50
- }
51
-
52
- const hookData = JSON.parse(input);
53
- const command = hookData.tool_input?.command || '';
54
-
55
- // If command already has yes/echo prefix, allow it
56
- const isSafe = SAFE_PATTERNS.some(pattern => pattern.test(command));
57
- if (isSafe) {
58
- process.exit(0);
59
- }
60
-
61
- // Check if this is a delete-risk command
62
- const isDeleteRisk = DELETE_RISK_PATTERNS.some(pattern => pattern.test(command));
63
-
64
- if (!isDeleteRisk) {
65
- process.exit(0);
66
- }
67
-
68
- // Output instructions to stderr (appears as system reminder)
69
- // This ensures Claude sees and follows the instructions
70
- const instructions = `============================================================
71
- ⚠️ MANDATORY ACTION REQUIRED - DO NOT SKIP
72
- ============================================================
73
-
74
- STOP! You MUST use the AskUserQuestion tool BEFORE running this command.
75
- This is a DESTRUCTIVE operation that may DELETE resources from Hailer.
76
-
77
- USE THIS EXACT AskUserQuestion CALL:
78
-
79
- \`\`\`json
80
- {
81
- "questions": [{
82
- "question": "This command may delete resources from Hailer. Items removed from local config will be PERMANENTLY deleted. Proceed with: ${command}?",
83
- "header": "Confirm Delete",
84
- "options": [
85
- { "label": "Yes, delete", "description": "Proceed with the destructive operation" },
86
- { "label": "No, cancel", "description": "Abort - don't delete anything" }
87
- ],
88
- "multiSelect": false
89
- }]
90
- }
91
- \`\`\`
92
-
93
- After user responds:
94
- - If "Yes, delete": Run: yes | ${command}
95
- - If "No, cancel": Acknowledge cancellation, do NOT run the command
96
-
97
- ============================================================
98
- REMEMBER: ASK FIRST using AskUserQuestion, then act based on response.
99
- ============================================================`;
100
-
101
- // Output to stderr so it appears as system reminder
102
- console.error(instructions);
103
-
104
- // Block the command - Claude must ask first
105
- const response = {
106
- permissionDecision: "deny",
107
- permissionDecisionReason: "Command blocked. Follow the instructions above to ask user for confirmation first."
108
- };
109
-
110
- console.log(JSON.stringify(response));
111
- process.exit(0);
112
-
113
- } catch (error) {
114
- console.error(`[sdk-delete-guard] Error: ${error.message}`);
115
- process.exit(0);
116
- }
117
- }
118
-
119
- main();