Refactor hooks codebase: reduce complexity and improve maintainability (#204)
* refactor: Clean up hook-response and new-hook files - Removed 'PreCompact' hook type and associated logic from hook-response.ts for improved type safety. - Deleted extensive architecture comments in new-hook.ts to streamline code readability. - Simplified debug logging in new-hook.ts to reduce verbosity. - Enhanced ensureWorkerRunning function in worker-utils.ts with a final health check before throwing errors. - Added a new documentation file outlining the hooks cleanup process and future improvements. * Refactor cleanup and user message hooks - Updated cleanup-hook.js to improve error handling and remove unnecessary input checks. - Simplified user-message-hook.js by removing time-sensitive announcements and streamlining output. - Enhanced logging functionality in both hooks for better debugging and clarity. * Refactor error handling in hooks to use centralized error handler - Introduced `handleWorkerError` function in `src/shared/hook-error-handler.ts` to manage worker-related errors. - Updated `context-hook.ts`, `new-hook.ts`, `save-hook.ts`, and `summary-hook.ts` to utilize the new error handler, simplifying error management and improving code readability. - Removed repetitive error handling logic from individual hooks, ensuring consistent user-friendly messages for connection issues. * Refactor user-message and summary hooks to utilize shared transcript parser; introduce hook exit codes - Moved user message extraction logic to a new shared module `transcript-parser.ts` for better code reuse. - Updated `summary-hook.ts` to use the new `extractLastMessage` function for retrieving user and assistant messages. - Replaced direct exit code usage in `user-message-hook.ts` with constants from `hook-constants.ts` for improved readability and maintainability. - Added `HOOK_EXIT_CODES` to `hook-constants.ts` to standardize exit codes across hooks. * Refactor hook input interfaces to enforce required fields - Updated `SessionStartInput`, `UserPromptSubmitInput`, `PostToolUseInput`, and `StopInput` interfaces to require `session_id`, `transcript_path`, and `cwd` fields, ensuring better type safety and clarity in hook inputs. - Removed optional index signatures from these interfaces to prevent unintended properties and improve code maintainability. - Adjusted related hook implementations to align with the new interface definitions. * Refactor save-hook to remove tool skipping logic; enhance summary-hook to handle spinner stopping with error logging; update SessionRoutes to load skip tools from settings; add CLAUDE_MEM_SKIP_TOOLS to SettingsDefaultsManager for configurable tool exclusion. * Document CLAUDE_MEM_SKIP_TOOLS setting in public docs Added documentation for the new CLAUDE_MEM_SKIP_TOOLS configuration setting in response to PR review feedback. Users can now discover and customize which tools are excluded from observations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,9 +13,6 @@ import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||
|
||||
export interface SessionEndInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
transcript_path?: string;
|
||||
hook_event_name: string;
|
||||
reason: 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other';
|
||||
}
|
||||
|
||||
@@ -28,22 +25,11 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
||||
|
||||
happy_path_error__with_fallback('[cleanup-hook] Hook fired', {
|
||||
session_id: input?.session_id,
|
||||
cwd: input?.cwd,
|
||||
reason: input?.reason
|
||||
});
|
||||
|
||||
// Handle standalone execution (no input provided)
|
||||
if (!input) {
|
||||
console.log('No input provided - this script is designed to run as a Claude Code SessionEnd hook');
|
||||
console.log('\nExpected input format:');
|
||||
console.log(JSON.stringify({
|
||||
session_id: "string",
|
||||
cwd: "string",
|
||||
transcript_path: "string",
|
||||
hook_event_name: "SessionEnd",
|
||||
reason: "exit"
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
throw new Error('cleanup-hook requires input from Claude Code');
|
||||
}
|
||||
|
||||
const { session_id, reason } = input;
|
||||
|
||||
@@ -10,14 +10,13 @@ import path from "path";
|
||||
import { stdin } from "process";
|
||||
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
|
||||
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
|
||||
import { handleWorkerError } from "../shared/hook-error-handler.js";
|
||||
|
||||
export interface SessionStartInput {
|
||||
session_id?: string;
|
||||
transcript_path?: string;
|
||||
cwd?: string;
|
||||
session_id: string;
|
||||
transcript_path: string;
|
||||
cwd: string;
|
||||
hook_event_name?: string;
|
||||
source?: "startup" | "resume" | "clear" | "compact";
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
async function contextHook(input?: SessionStartInput): Promise<string> {
|
||||
@@ -41,11 +40,7 @@ async function contextHook(input?: SessionStartInput): Promise<string> {
|
||||
const result = await response.text();
|
||||
return result.trim();
|
||||
} catch (error: any) {
|
||||
// Only show restart message for connection errors, not HTTP errors
|
||||
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
|
||||
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
|
||||
}
|
||||
throw error;
|
||||
handleWorkerError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type HookType = 'PreCompact' | 'SessionStart' | 'UserPromptSubmit' | 'PostToolUse' | 'Stop' | string;
|
||||
export type HookType = 'SessionStart' | 'UserPromptSubmit' | 'PostToolUse' | 'Stop';
|
||||
|
||||
export interface HookResponseOptions {
|
||||
reason?: string;
|
||||
@@ -20,21 +20,6 @@ function buildHookResponse(
|
||||
success: boolean,
|
||||
options: HookResponseOptions
|
||||
): HookResponse {
|
||||
if (hookType === 'PreCompact') {
|
||||
if (success) {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
continue: false,
|
||||
stopReason: options.reason || 'Pre-compact operation failed',
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
if (hookType === 'SessionStart') {
|
||||
if (success && options.context) {
|
||||
return {
|
||||
|
||||
+5
-57
@@ -1,49 +1,14 @@
|
||||
/**
|
||||
* New Hook - UserPromptSubmit
|
||||
*
|
||||
* DUAL PURPOSE HOOK: Handles BOTH session initialization AND continuation
|
||||
* ==========================================================================
|
||||
*
|
||||
* CRITICAL ARCHITECTURE FACTS (NEVER FORGET):
|
||||
*
|
||||
* 1. SESSION ID THREADING - The Single Source of Truth
|
||||
* - Claude Code assigns ONE session_id per conversation
|
||||
* - ALL hooks in that conversation receive the SAME session_id
|
||||
* - We ALWAYS use this session_id - NEVER generate our own
|
||||
* - This is how NEW hook, SAVE hook, and SUMMARY hook stay connected
|
||||
*
|
||||
* 2. NO EXISTENCE CHECKS NEEDED
|
||||
* - createSDKSession is idempotent (INSERT OR IGNORE)
|
||||
* - Prompt #1: Creates new database row, returns new ID
|
||||
* - Prompt #2+: Row exists, returns existing ID
|
||||
* - We NEVER need to check "does session exist?" - just use the session_id
|
||||
*
|
||||
* 3. CONTINUATION LOGIC LOCATION
|
||||
* - This hook does NOT contain continuation prompt logic
|
||||
* - That lives in SDKAgent.ts (lines 125-127)
|
||||
* - SDKAgent checks promptNumber to choose init vs continuation prompt
|
||||
* - BOTH prompts receive the SAME session_id from this hook
|
||||
*
|
||||
* 4. UNIFIED WITH SAVE HOOK
|
||||
* - SAVE hook uses: db.createSDKSession(session_id, '', '')
|
||||
* - NEW hook uses: db.createSDKSession(session_id, project, prompt)
|
||||
* - Both use session_id from hook context - this keeps everything connected
|
||||
*
|
||||
* This is KISS in action: Use the session_id we're given, trust idempotent
|
||||
* database operations, and let SDKAgent handle init vs continuation logic.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { stdin } from 'process';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
|
||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||
|
||||
export interface UserPromptSubmitInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
prompt: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,25 +24,12 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
}
|
||||
|
||||
const { session_id, cwd, prompt } = input;
|
||||
|
||||
// Debug: Log what we received
|
||||
happy_path_error__with_fallback('[new-hook] Input received', {
|
||||
session_id,
|
||||
cwd,
|
||||
cwd_type: typeof cwd,
|
||||
cwd_length: cwd?.length,
|
||||
has_cwd: !!cwd,
|
||||
prompt_length: prompt?.length
|
||||
});
|
||||
|
||||
const project = path.basename(cwd);
|
||||
|
||||
happy_path_error__with_fallback('[new-hook] Project extracted', {
|
||||
happy_path_error__with_fallback('[new-hook] Input received', {
|
||||
session_id,
|
||||
project,
|
||||
project_type: typeof project,
|
||||
project_length: project?.length,
|
||||
is_empty: project === '',
|
||||
cwd_was: cwd
|
||||
prompt_length: prompt?.length
|
||||
});
|
||||
|
||||
const port = getWorkerPort();
|
||||
@@ -116,11 +68,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
|
||||
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
} catch (error: any) {
|
||||
// Only show restart message for connection errors, not HTTP errors
|
||||
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
|
||||
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
|
||||
}
|
||||
throw error;
|
||||
handleWorkerError(error);
|
||||
}
|
||||
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
|
||||
+2
-20
@@ -12,6 +12,7 @@ import { logger } from '../utils/logger.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
|
||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||
|
||||
export interface PostToolUseInput {
|
||||
session_id: string;
|
||||
@@ -19,18 +20,8 @@ export interface PostToolUseInput {
|
||||
tool_name: string;
|
||||
tool_input: any;
|
||||
tool_response: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Tools to skip (low value or too frequent)
|
||||
const SKIP_TOOLS = new Set([
|
||||
'ListMcpResourcesTool', // MCP infrastructure
|
||||
'SlashCommand', // Command invocation (observe what it produces, not the call)
|
||||
'Skill', // Skill invocation (observe what it produces, not the call)
|
||||
'TodoWrite', // Task management meta-tool
|
||||
'AskUserQuestion' // User interaction, not substantive work
|
||||
]);
|
||||
|
||||
/**
|
||||
* Save Hook Main Logic - Fire-and-forget HTTP client
|
||||
*/
|
||||
@@ -44,11 +35,6 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
|
||||
const { session_id, cwd, tool_name, tool_input, tool_response } = input;
|
||||
|
||||
if (SKIP_TOOLS.has(tool_name)) {
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
return;
|
||||
}
|
||||
|
||||
const port = getWorkerPort();
|
||||
|
||||
const toolStr = logger.formatTool(tool_name, tool_input);
|
||||
@@ -86,11 +72,7 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
|
||||
logger.debug('HOOK', 'Observation sent successfully', { toolName: tool_name });
|
||||
} catch (error: any) {
|
||||
// Only show restart message for connection errors, not HTTP errors
|
||||
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
|
||||
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
|
||||
}
|
||||
throw error;
|
||||
handleWorkerError(error);
|
||||
}
|
||||
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
|
||||
+20
-120
@@ -10,122 +10,18 @@
|
||||
*/
|
||||
|
||||
import { stdin } from 'process';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
|
||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||
import { extractLastMessage } from '../shared/transcript-parser.js';
|
||||
|
||||
export interface StopInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
transcript_path?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract last user message from transcript JSONL file
|
||||
*/
|
||||
function extractLastUserMessage(transcriptPath: string): string {
|
||||
if (!transcriptPath || !existsSync(transcriptPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(transcriptPath, 'utf-8').trim();
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Parse JSONL and find last user message
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const line = JSON.parse(lines[i]);
|
||||
|
||||
// Claude Code transcript format: {type: "user", message: {role: "user", content: [...]}}
|
||||
if (line.type === 'user' && line.message?.content) {
|
||||
const content = line.message.content;
|
||||
|
||||
// Extract text content (handle both string and array formats)
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
} else if (Array.isArray(content)) {
|
||||
const textParts = content
|
||||
.filter((c: any) => c.type === 'text')
|
||||
.map((c: any) => c.text);
|
||||
return textParts.join('\n');
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip malformed lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('HOOK', 'Failed to read transcript', { transcriptPath }, error as Error);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract last assistant message from transcript JSONL file
|
||||
* Filters out system-reminder tags to avoid polluting summaries
|
||||
*/
|
||||
function extractLastAssistantMessage(transcriptPath: string): string {
|
||||
if (!transcriptPath || !existsSync(transcriptPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(transcriptPath, 'utf-8').trim();
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Parse JSONL and find last assistant message
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const line = JSON.parse(lines[i]);
|
||||
|
||||
// Claude Code transcript format: {type: "assistant", message: {role: "assistant", content: [...]}}
|
||||
if (line.type === 'assistant' && line.message?.content) {
|
||||
let text = '';
|
||||
const content = line.message.content;
|
||||
|
||||
// Extract text content (handle both string and array formats)
|
||||
if (typeof content === 'string') {
|
||||
text = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
const textParts = content
|
||||
.filter((c: any) => c.type === 'text')
|
||||
.map((c: any) => c.text);
|
||||
text = textParts.join('\n');
|
||||
}
|
||||
|
||||
// Filter out system-reminder tags and their content
|
||||
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '');
|
||||
|
||||
// Clean up excessive whitespace
|
||||
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip malformed lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('HOOK', 'Failed to read transcript', { transcriptPath }, error as Error);
|
||||
}
|
||||
|
||||
return '';
|
||||
transcript_path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,8 +45,8 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
||||
{ session_id },
|
||||
input.transcript_path || ''
|
||||
);
|
||||
const lastUserMessage = extractLastUserMessage(transcriptPath);
|
||||
const lastAssistantMessage = extractLastAssistantMessage(transcriptPath);
|
||||
const lastUserMessage = extractLastMessage(transcriptPath, 'user');
|
||||
const lastAssistantMessage = extractLastMessage(transcriptPath, 'assistant', true);
|
||||
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
workerPort: port,
|
||||
@@ -181,18 +77,22 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
||||
|
||||
logger.debug('HOOK', 'Summary request sent successfully');
|
||||
} catch (error: any) {
|
||||
// Only show restart message for connection errors, not HTTP errors
|
||||
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
|
||||
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
|
||||
}
|
||||
throw error;
|
||||
handleWorkerError(error);
|
||||
} finally {
|
||||
// Notify worker to stop spinner (fire-and-forget)
|
||||
fetch(`http://127.0.0.1:${port}/api/processing`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isProcessing: false })
|
||||
}).catch(() => {});
|
||||
// Stop processing spinner
|
||||
try {
|
||||
const spinnerResponse = await fetch(`http://127.0.0.1:${port}/api/processing`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isProcessing: false }),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
if (!spinnerResponse.ok) {
|
||||
logger.warn('HOOK', 'Failed to stop spinner', { status: spinnerResponse.status });
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.warn('HOOK', 'Could not stop spinner', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(createHookResponse('Stop', true));
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
import { basename } from "path";
|
||||
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
|
||||
import { HOOK_EXIT_CODES } from "../shared/hook-constants.js";
|
||||
|
||||
try {
|
||||
// Ensure worker is running
|
||||
@@ -28,55 +29,12 @@ try {
|
||||
|
||||
const output = await response.text();
|
||||
|
||||
// If it's after Dec 5, 2025 7pm EST, patch this out
|
||||
const now = new Date();
|
||||
const amaEndDate = new Date('2025-12-06T00:00:00Z'); // Dec 5, 2025 7pm EST
|
||||
|
||||
// Product Hunt launch announcement - expires Dec 5, 2025 12am EST (05:00 UTC)
|
||||
const phLaunchEndDate = new Date('2025-12-05T05:00:00Z');
|
||||
let productHuntAnnouncement = "";
|
||||
if (now < phLaunchEndDate) {
|
||||
productHuntAnnouncement = `
|
||||
|
||||
🚀 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🚀
|
||||
|
||||
We launched on Product Hunt!
|
||||
https://tinyurl.com/claude-mem-ph
|
||||
|
||||
⭐ Your upvote means the world - thank you!
|
||||
|
||||
🚀 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🚀
|
||||
`;
|
||||
}
|
||||
|
||||
let amaAnnouncement = "";
|
||||
if (now < amaEndDate) {
|
||||
// Check if we're during the live event (Dec 1-5, 5pm-7pm EST daily)
|
||||
const estOffset = 5 * 60; // EST is UTC-5
|
||||
const nowUtcMinutes = now.getUTCHours() * 60 + now.getUTCMinutes();
|
||||
const estHour = Math.floor((nowUtcMinutes - estOffset + 1440) % 1440 / 60);
|
||||
const day = now.getUTCDate();
|
||||
const month = now.getUTCMonth();
|
||||
const year = now.getUTCFullYear();
|
||||
|
||||
const isDec1to5 = year === 2025 && month === 11 && day >= 1 && day <= 5;
|
||||
const isDuringLiveHours = estHour >= 17 && estHour < 19; // 5pm-7pm EST
|
||||
|
||||
if (isDec1to5 && isDuringLiveHours) {
|
||||
amaAnnouncement = "\n 🔴 LIVE NOW: AMA w/ Dev (@thedotmack) until 7pm EST\n";
|
||||
} else {
|
||||
amaAnnouncement = "\n – LIVE AMA w/ Dev (@thedotmack) Dec 1st–5th, 5pm to 7pm EST\n";
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
"\n\n📝 Claude-Mem Context Loaded\n" +
|
||||
" ℹ️ Note: This appears as stderr but is informational only\n\n" +
|
||||
output +
|
||||
"\n\n💡 New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.\n" +
|
||||
"\n💬 Community https://discord.gg/J4wttp9vDu" +
|
||||
productHuntAnnouncement +
|
||||
amaAnnouncement +
|
||||
`\n📺 Watch live in browser http://localhost:${port}/\n`
|
||||
);
|
||||
|
||||
@@ -103,4 +61,4 @@ This message was not added to your startup context, so you can continue working
|
||||
`);
|
||||
}
|
||||
|
||||
process.exit(3);
|
||||
process.exit(HOOK_EXIT_CODES.USER_MESSAGE_ONLY);
|
||||
Reference in New Issue
Block a user