1111import { Command } from 'commander' ;
1212import chalk from 'chalk' ;
1313import 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' ;
1624import { spawn , execSync } from 'child_process' ;
1725
26+ const __filename = fileURLToPath ( import . meta. url ) ;
27+ const __dirname = dirname ( __filename ) ;
28+
1829interface 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+
64103async 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