Skip to content

Commit d2d52f1

Browse files
feat: Add sweep hook CLI command and improve hook behavior
1 parent 8986333 commit d2d52f1

4 files changed

Lines changed: 300 additions & 17 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackmemoryai/stackmemory",
3-
"version": "0.5.3",
3+
"version": "0.5.4",
44
"description": "Lossless memory runtime for AI coding tools - organizes context as a call stack instead of linear chat logs, with team collaboration and infinite retention",
55
"engines": {
66
"node": ">=20.0.0",

src/cli/commands/sweep.ts

Lines changed: 211 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,21 @@
1111
import { Command } from 'commander';
1212
import chalk from 'chalk';
1313
import ora from 'ora';
14-
import { existsSync, readFileSync } from 'fs';
15-
import { join } from 'path';
14+
import {
15+
existsSync,
16+
readFileSync,
17+
writeFileSync,
18+
mkdirSync,
19+
copyFileSync,
20+
chmodSync,
21+
} from 'fs';
22+
import { join, dirname } from 'path';
23+
import { fileURLToPath } from 'url';
1624
import { spawn, execSync } from 'child_process';
1725

26+
const __filename = fileURLToPath(import.meta.url);
27+
const __dirname = dirname(__filename);
28+
1829
interface SweepStatus {
1930
installed: boolean;
2031
model_downloaded: boolean;
@@ -45,7 +56,7 @@ function findPythonScript(): string | null {
4556
join(
4657
process.cwd(),
4758
'node_modules',
48-
'@stackmemory',
59+
'@stackmemoryai',
4960
'sweep-addon',
5061
'python',
5162
'sweep_predict.py'
@@ -61,6 +72,34 @@ function findPythonScript(): string | null {
6172
return null;
6273
}
6374

75+
function findHookSource(): string | null {
76+
const locations = [
77+
join(process.cwd(), 'templates', 'claude-hooks', 'post-edit-sweep.js'),
78+
join(
79+
process.cwd(),
80+
'node_modules',
81+
'@stackmemoryai',
82+
'stackmemory',
83+
'templates',
84+
'claude-hooks',
85+
'post-edit-sweep.js'
86+
),
87+
join(
88+
dirname(dirname(dirname(__dirname))),
89+
'templates',
90+
'claude-hooks',
91+
'post-edit-sweep.js'
92+
),
93+
];
94+
95+
for (const loc of locations) {
96+
if (existsSync(loc)) {
97+
return loc;
98+
}
99+
}
100+
return null;
101+
}
102+
64103
async function findPython(): Promise<string | null> {
65104
const candidates = ['python3', 'python'];
66105

@@ -375,6 +414,175 @@ hf_hub_download(
375414
}
376415
});
377416

417+
const hookCmd = cmd
418+
.command('hook')
419+
.description('Manage Claude Code integration hook');
420+
421+
hookCmd
422+
.command('install')
423+
.description('Install Sweep prediction hook for Claude Code')
424+
.action(async () => {
425+
const spinner = ora('Installing Sweep hook...').start();
426+
427+
const homeDir = process.env.HOME || '';
428+
const hookDir = join(homeDir, '.claude', 'hooks');
429+
const sweepDir = join(homeDir, '.stackmemory', 'sweep');
430+
const hooksJsonPath = join(homeDir, '.claude', 'hooks.json');
431+
432+
try {
433+
mkdirSync(hookDir, { recursive: true });
434+
mkdirSync(sweepDir, { recursive: true });
435+
436+
const hookSource = findHookSource();
437+
if (!hookSource) {
438+
spinner.fail(chalk.red('Hook template not found'));
439+
console.log(
440+
chalk.gray('Ensure stackmemory is installed from the repository')
441+
);
442+
process.exit(1);
443+
}
444+
445+
const hookDest = join(hookDir, 'post-edit-sweep.js');
446+
copyFileSync(hookSource, hookDest);
447+
chmodSync(hookDest, '755');
448+
449+
const pythonScriptSource = findPythonScript();
450+
if (pythonScriptSource) {
451+
const pythonDest = join(sweepDir, 'sweep_predict.py');
452+
copyFileSync(pythonScriptSource, pythonDest);
453+
}
454+
455+
if (existsSync(hooksJsonPath)) {
456+
const hooks = JSON.parse(readFileSync(hooksJsonPath, 'utf-8'));
457+
if (!hooks['post-tool-use']) {
458+
hooks['post-tool-use'] = hookDest;
459+
writeFileSync(hooksJsonPath, JSON.stringify(hooks, null, 2));
460+
} else if (!hooks['post-tool-use'].includes('sweep')) {
461+
spinner.warn(chalk.yellow('post-tool-use hook already configured'));
462+
console.log(chalk.gray(`Existing: ${hooks['post-tool-use']}`));
463+
console.log(chalk.gray(`Hook installed at: ${hookDest}`));
464+
console.log(
465+
chalk.gray('You may need to manually configure the hook chain')
466+
);
467+
return;
468+
}
469+
} else {
470+
const hooks = { 'post-tool-use': hookDest };
471+
writeFileSync(hooksJsonPath, JSON.stringify(hooks, null, 2));
472+
}
473+
474+
spinner.succeed(chalk.green('Sweep hook installed'));
475+
console.log(chalk.gray(`Hook: ${hookDest}`));
476+
console.log(chalk.gray(`Config: ${hooksJsonPath}`));
477+
console.log('');
478+
console.log(chalk.bold('Usage:'));
479+
console.log(' Hook runs automatically after Edit/Write operations');
480+
console.log(' Predictions appear after 2+ edits in session');
481+
console.log(' Disable: export SWEEP_ENABLED=false');
482+
} catch (error) {
483+
spinner.fail(chalk.red('Installation failed'));
484+
console.log(chalk.gray((error as Error).message));
485+
process.exit(1);
486+
}
487+
});
488+
489+
hookCmd
490+
.command('status')
491+
.description('Check hook installation status')
492+
.action(async () => {
493+
const homeDir = process.env.HOME || '';
494+
const hookPath = join(homeDir, '.claude', 'hooks', 'post-edit-sweep.js');
495+
const hooksJsonPath = join(homeDir, '.claude', 'hooks.json');
496+
const statePath = join(homeDir, '.stackmemory', 'sweep-state.json');
497+
498+
console.log(chalk.bold('\nSweep Hook Status\n'));
499+
500+
const hookInstalled = existsSync(hookPath);
501+
console.log(
502+
`Hook installed: ${hookInstalled ? chalk.green('Yes') : chalk.yellow('No')}`
503+
);
504+
505+
if (existsSync(hooksJsonPath)) {
506+
const hooks = JSON.parse(readFileSync(hooksJsonPath, 'utf-8'));
507+
const configured =
508+
hooks['post-tool-use'] && hooks['post-tool-use'].includes('sweep');
509+
console.log(
510+
`Hook configured: ${configured ? chalk.green('Yes') : chalk.yellow('No')}`
511+
);
512+
} else {
513+
console.log(`Hook configured: ${chalk.yellow('No hooks.json')}`);
514+
}
515+
516+
const enabled = process.env.SWEEP_ENABLED !== 'false';
517+
console.log(
518+
`Enabled: ${enabled ? chalk.green('Yes') : chalk.yellow('Disabled (SWEEP_ENABLED=false)')}`
519+
);
520+
521+
if (existsSync(statePath)) {
522+
try {
523+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
524+
console.log(
525+
chalk.gray(
526+
`\nRecent diffs tracked: ${state.recentDiffs?.length || 0}`
527+
)
528+
);
529+
if (state.lastPrediction) {
530+
const age = Date.now() - state.lastPrediction.timestamp;
531+
const ageStr =
532+
age < 60000
533+
? `${Math.round(age / 1000)}s ago`
534+
: `${Math.round(age / 60000)}m ago`;
535+
console.log(chalk.gray(`Last prediction: ${ageStr}`));
536+
}
537+
} catch {
538+
// Ignore parse errors
539+
}
540+
}
541+
542+
if (!hookInstalled) {
543+
console.log(chalk.bold('\nTo install: stackmemory sweep hook install'));
544+
}
545+
});
546+
547+
hookCmd
548+
.command('disable')
549+
.description('Disable the Sweep hook')
550+
.action(() => {
551+
console.log(chalk.bold('\nTo disable Sweep predictions:\n'));
552+
console.log(' Temporarily: export SWEEP_ENABLED=false');
553+
console.log(' Permanently: Add to ~/.zshrc or ~/.bashrc');
554+
console.log('');
555+
console.log('Or remove the hook:');
556+
console.log(' rm ~/.claude/hooks/post-edit-sweep.js');
557+
});
558+
559+
hookCmd
560+
.command('clear')
561+
.description('Clear hook state (recent diffs and predictions)')
562+
.action(() => {
563+
const homeDir = process.env.HOME || '';
564+
const statePath = join(homeDir, '.stackmemory', 'sweep-state.json');
565+
566+
if (existsSync(statePath)) {
567+
writeFileSync(
568+
statePath,
569+
JSON.stringify(
570+
{
571+
recentDiffs: [],
572+
lastPrediction: null,
573+
pendingPrediction: null,
574+
fileContents: {},
575+
},
576+
null,
577+
2
578+
)
579+
);
580+
console.log(chalk.green('Sweep state cleared'));
581+
} else {
582+
console.log(chalk.gray('No state file found'));
583+
}
584+
});
585+
378586
cmd.action(async () => {
379587
const status = await checkSweepStatus();
380588
console.log(chalk.bold('\nSweep 1.5B Addon Status\n'));

0 commit comments

Comments
 (0)