Skip to content

Commit 6db88fe

Browse files
feat: Add auto-background hook for long-running commands
- Auto-backgrounds builds, tests, installs, docker commands - Configurable via stackmemory auto-bg CLI - Claude Code hook for pre-tool-use interception - Patterns for always/never background commands
1 parent b5c6dbd commit 6db88fe

6 files changed

Lines changed: 716 additions & 0 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/bin/bash
2+
# Install auto-background hook for Claude Code
3+
4+
set -e
5+
6+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
HOOK_SOURCE="$SCRIPT_DIR/../templates/claude-hooks/auto-background-hook.js"
8+
CLAUDE_DIR="$HOME/.claude"
9+
HOOKS_DIR="$CLAUDE_DIR/hooks"
10+
SETTINGS_FILE="$CLAUDE_DIR/settings.json"
11+
CONFIG_DIR="$HOME/.stackmemory"
12+
13+
echo "Installing auto-background hook for Claude Code..."
14+
15+
# Create directories
16+
mkdir -p "$HOOKS_DIR"
17+
mkdir -p "$CONFIG_DIR"
18+
19+
# Copy hook script
20+
HOOK_DEST="$HOOKS_DIR/auto-background-hook.js"
21+
cp "$HOOK_SOURCE" "$HOOK_DEST"
22+
chmod +x "$HOOK_DEST"
23+
echo "Installed hook to $HOOK_DEST"
24+
25+
# Create default config if not exists
26+
CONFIG_FILE="$CONFIG_DIR/auto-background.json"
27+
if [ ! -f "$CONFIG_FILE" ]; then
28+
cat > "$CONFIG_FILE" << 'EOF'
29+
{
30+
"enabled": true,
31+
"timeoutMs": 5000,
32+
"alwaysBackground": [
33+
"npm install",
34+
"npm ci",
35+
"yarn install",
36+
"pnpm install",
37+
"bun install",
38+
"npm run build",
39+
"yarn build",
40+
"pnpm build",
41+
"cargo build",
42+
"go build",
43+
"make",
44+
"npm test",
45+
"npm run test",
46+
"yarn test",
47+
"pytest",
48+
"jest",
49+
"vitest",
50+
"cargo test",
51+
"docker build",
52+
"docker-compose up",
53+
"docker compose up",
54+
"git clone",
55+
"git fetch --all",
56+
"npx tsc",
57+
"tsc --noEmit",
58+
"eslint .",
59+
"npm run lint"
60+
],
61+
"neverBackground": [
62+
"vim",
63+
"nvim",
64+
"nano",
65+
"less",
66+
"more",
67+
"top",
68+
"htop",
69+
"echo",
70+
"cat",
71+
"ls",
72+
"pwd",
73+
"cd",
74+
"which",
75+
"git status",
76+
"git diff",
77+
"git log"
78+
],
79+
"verbose": false
80+
}
81+
EOF
82+
echo "Created config at $CONFIG_FILE"
83+
fi
84+
85+
# Update Claude Code settings
86+
if [ -f "$SETTINGS_FILE" ]; then
87+
# Check if jq is available
88+
if command -v jq &> /dev/null; then
89+
# Backup existing settings
90+
cp "$SETTINGS_FILE" "$SETTINGS_FILE.bak"
91+
92+
# Add hook to settings
93+
HOOK_CMD="node $HOOK_DEST"
94+
95+
# Check if hooks.pre_tool_use exists
96+
if jq -e '.hooks.pre_tool_use' "$SETTINGS_FILE" > /dev/null 2>&1; then
97+
# Check if hook already added
98+
if ! jq -e ".hooks.pre_tool_use | index(\"$HOOK_CMD\")" "$SETTINGS_FILE" > /dev/null 2>&1; then
99+
jq ".hooks.pre_tool_use += [\"$HOOK_CMD\"]" "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
100+
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
101+
echo "Added hook to existing pre_tool_use array"
102+
else
103+
echo "Hook already configured"
104+
fi
105+
else
106+
# Create hooks.pre_tool_use array
107+
jq ".hooks = (.hooks // {}) | .hooks.pre_tool_use = [\"$HOOK_CMD\"]" "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
108+
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
109+
echo "Created hooks.pre_tool_use with auto-background hook"
110+
fi
111+
else
112+
echo ""
113+
echo "jq not found. Please manually add to $SETTINGS_FILE:"
114+
echo ""
115+
echo ' "hooks": {'
116+
echo ' "pre_tool_use": ["node '$HOOK_DEST'"]'
117+
echo ' }'
118+
fi
119+
else
120+
# Create new settings file
121+
cat > "$SETTINGS_FILE" << EOF
122+
{
123+
"hooks": {
124+
"pre_tool_use": ["node $HOOK_DEST"]
125+
}
126+
}
127+
EOF
128+
echo "Created settings file with hook"
129+
fi
130+
131+
echo ""
132+
echo "Auto-background hook installed!"
133+
echo ""
134+
echo "Configuration: $CONFIG_FILE"
135+
echo " - Edit to customize which commands auto-background"
136+
echo " - Set 'enabled': false to disable"
137+
echo " - Set 'verbose': true for debug logging"
138+
echo ""
139+
echo "Commands that will auto-background:"
140+
echo " - npm install/build/test"
141+
echo " - yarn/pnpm/bun install"
142+
echo " - docker build"
143+
echo " - cargo/go build/test"
144+
echo " - And more (see config)"
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/**
2+
* CLI command for managing auto-background settings
3+
*/
4+
5+
import { Command } from 'commander';
6+
import chalk from 'chalk';
7+
import { execSync } from 'child_process';
8+
import { join } from 'path';
9+
import {
10+
loadConfig,
11+
saveConfig,
12+
AutoBackgroundConfig,
13+
} from '../../hooks/auto-background.js';
14+
15+
// __dirname provided by esbuild banner
16+
17+
export function createAutoBackgroundCommand(): Command {
18+
const cmd = new Command('auto-bg')
19+
.description('Manage auto-background settings for long-running commands')
20+
.addHelpText(
21+
'after',
22+
`
23+
Examples:
24+
stackmemory auto-bg show Show current configuration
25+
stackmemory auto-bg enable Enable auto-backgrounding
26+
stackmemory auto-bg disable Disable auto-backgrounding
27+
stackmemory auto-bg add "npm publish" Add command to always-background list
28+
stackmemory auto-bg remove "npm test" Remove command from list
29+
stackmemory auto-bg timeout 10000 Set timeout to 10 seconds
30+
stackmemory auto-bg install Install Claude Code hook
31+
`
32+
);
33+
34+
cmd
35+
.command('show')
36+
.description('Show current auto-background configuration')
37+
.action(() => {
38+
const config = loadConfig();
39+
console.log(chalk.blue('Auto-Background Configuration:'));
40+
console.log();
41+
console.log(
42+
` ${chalk.gray('Enabled:')} ${config.enabled ? chalk.green('yes') : chalk.red('no')}`
43+
);
44+
console.log(` ${chalk.gray('Timeout:')} ${config.timeoutMs}ms`);
45+
console.log(
46+
` ${chalk.gray('Verbose:')} ${config.verbose ? 'yes' : 'no'}`
47+
);
48+
console.log();
49+
console.log(chalk.blue('Always Background:'));
50+
config.alwaysBackground.forEach((p) => console.log(` - ${p}`));
51+
console.log();
52+
console.log(chalk.blue('Never Background:'));
53+
config.neverBackground.forEach((p) => console.log(` - ${p}`));
54+
});
55+
56+
cmd
57+
.command('enable')
58+
.description('Enable auto-backgrounding')
59+
.action(() => {
60+
const config = loadConfig();
61+
config.enabled = true;
62+
saveConfig(config);
63+
console.log(chalk.green('Auto-background enabled'));
64+
});
65+
66+
cmd
67+
.command('disable')
68+
.description('Disable auto-backgrounding')
69+
.action(() => {
70+
const config = loadConfig();
71+
config.enabled = false;
72+
saveConfig(config);
73+
console.log(chalk.yellow('Auto-background disabled'));
74+
});
75+
76+
cmd
77+
.command('add <pattern>')
78+
.description('Add command pattern to always-background list')
79+
.action((pattern: string) => {
80+
const config = loadConfig();
81+
if (!config.alwaysBackground.includes(pattern)) {
82+
config.alwaysBackground.push(pattern);
83+
saveConfig(config);
84+
console.log(chalk.green(`Added: ${pattern}`));
85+
} else {
86+
console.log(chalk.yellow(`Already in list: ${pattern}`));
87+
}
88+
});
89+
90+
cmd
91+
.command('remove <pattern>')
92+
.description('Remove command pattern from always-background list')
93+
.action((pattern: string) => {
94+
const config = loadConfig();
95+
const idx = config.alwaysBackground.indexOf(pattern);
96+
if (idx !== -1) {
97+
config.alwaysBackground.splice(idx, 1);
98+
saveConfig(config);
99+
console.log(chalk.green(`Removed: ${pattern}`));
100+
} else {
101+
console.log(chalk.yellow(`Not in list: ${pattern}`));
102+
}
103+
});
104+
105+
cmd
106+
.command('timeout <ms>')
107+
.description('Set timeout threshold in milliseconds')
108+
.action((ms: string) => {
109+
const config = loadConfig();
110+
const timeout = parseInt(ms, 10);
111+
if (isNaN(timeout) || timeout < 0) {
112+
console.log(chalk.red('Invalid timeout value'));
113+
return;
114+
}
115+
config.timeoutMs = timeout;
116+
saveConfig(config);
117+
console.log(chalk.green(`Timeout set to ${timeout}ms`));
118+
});
119+
120+
cmd
121+
.command('verbose [on|off]')
122+
.description('Enable/disable verbose logging')
123+
.action((value?: string) => {
124+
const config = loadConfig();
125+
if (value === undefined) {
126+
config.verbose = !config.verbose;
127+
} else {
128+
config.verbose = value === 'on' || value === 'true';
129+
}
130+
saveConfig(config);
131+
console.log(
132+
chalk.green(
133+
`Verbose logging ${config.verbose ? 'enabled' : 'disabled'}`
134+
)
135+
);
136+
});
137+
138+
cmd
139+
.command('reset')
140+
.description('Reset configuration to defaults')
141+
.action(() => {
142+
const defaultConfig: AutoBackgroundConfig = {
143+
enabled: true,
144+
timeoutMs: 5000,
145+
alwaysBackground: [
146+
'npm install',
147+
'npm ci',
148+
'yarn install',
149+
'pnpm install',
150+
'bun install',
151+
'npm run build',
152+
'yarn build',
153+
'pnpm build',
154+
'cargo build',
155+
'go build',
156+
'make',
157+
'npm test',
158+
'npm run test',
159+
'yarn test',
160+
'pytest',
161+
'jest',
162+
'vitest',
163+
'cargo test',
164+
'docker build',
165+
'docker-compose up',
166+
'docker compose up',
167+
'git clone',
168+
'git fetch --all',
169+
'npx tsc',
170+
'tsc --noEmit',
171+
'eslint .',
172+
'npm run lint',
173+
],
174+
neverBackground: [
175+
'vim',
176+
'nvim',
177+
'nano',
178+
'less',
179+
'more',
180+
'top',
181+
'htop',
182+
'echo',
183+
'cat',
184+
'ls',
185+
'pwd',
186+
'cd',
187+
'which',
188+
'git status',
189+
'git diff',
190+
'git log',
191+
],
192+
verbose: false,
193+
};
194+
saveConfig(defaultConfig);
195+
console.log(chalk.green('Configuration reset to defaults'));
196+
});
197+
198+
cmd
199+
.command('install')
200+
.description('Install Claude Code hook for auto-backgrounding')
201+
.action(() => {
202+
try {
203+
// Find the install script
204+
const scriptPath = join(
205+
__dirname,
206+
'../../../scripts/install-auto-background-hook.sh'
207+
);
208+
execSync(`bash "${scriptPath}"`, { stdio: 'inherit' });
209+
} catch {
210+
console.error(chalk.red('Failed to install hook'));
211+
console.log(
212+
chalk.gray(
213+
'Run manually: bash scripts/install-auto-background-hook.sh'
214+
)
215+
);
216+
}
217+
});
218+
219+
return cmd;
220+
}

src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { createHooksCommand } from './commands/hooks.js';
5353
import { createShellCommand } from './commands/shell.js';
5454
import { createAPICommand } from './commands/api.js';
5555
import { createCleanupProcessesCommand } from './commands/cleanup-processes.js';
56+
import { createAutoBackgroundCommand } from './commands/auto-background.js';
5657
import { ProjectManager } from '../core/projects/project-manager.js';
5758
import Database from 'better-sqlite3';
5859
import { join } from 'path';
@@ -584,6 +585,7 @@ program.addCommand(createHooksCommand());
584585
program.addCommand(createShellCommand());
585586
program.addCommand(createAPICommand());
586587
program.addCommand(createCleanupProcessesCommand());
588+
program.addCommand(createAutoBackgroundCommand());
587589

588590
// Register dashboard command
589591
program

0 commit comments

Comments
 (0)