@@ -1788,23 +1788,46 @@ icm store -t \"topic\" -c \"summary\"
17881788
17891789 // PreToolUse hook: `icm hook pre` (auto-allow icm commands)
17901790 let pre_cmd = format ! ( "{} hook pre" , icm_bin_str) ;
1791- let pre_status = inject_claude_pretool_hook ( & claude_settings_path, & pre_cmd) ?;
1791+ let pre_status = inject_claude_hook (
1792+ & claude_settings_path,
1793+ "PreToolUse" ,
1794+ & pre_cmd,
1795+ Some ( "Bash" ) ,
1796+ & [ "icm-pretool" , "icm hook pre" ] ,
1797+ ) ?;
17921798 println ! ( "[hook] Claude Code PreToolUse (auto-allow): {pre_status}" ) ;
17931799
17941800 // PostToolUse hook: `icm hook post` (auto-extract context)
17951801 let post_cmd = format ! ( "{} hook post" , icm_bin_str) ;
1796- let post_status = inject_claude_hook ( & claude_settings_path, "PostToolUse" , & post_cmd) ?;
1802+ let post_status = inject_claude_hook (
1803+ & claude_settings_path,
1804+ "PostToolUse" ,
1805+ & post_cmd,
1806+ None ,
1807+ & [ "icm hook" , "icm-post-tool" ] ,
1808+ ) ?;
17971809 println ! ( "[hook] Claude Code PostToolUse (auto-extract): {post_status}" ) ;
17981810
17991811 // PreCompact hook: `icm hook compact` (extract from transcript before compression)
18001812 let compact_cmd = format ! ( "{} hook compact" , icm_bin_str) ;
1801- let compact_status = inject_claude_hook ( & claude_settings_path, "PreCompact" , & compact_cmd) ?;
1813+ let compact_status = inject_claude_hook (
1814+ & claude_settings_path,
1815+ "PreCompact" ,
1816+ & compact_cmd,
1817+ None ,
1818+ & [ "icm hook" , "icm-post-tool" ] ,
1819+ ) ?;
18021820 println ! ( "[hook] Claude Code PreCompact (transcript extract): {compact_status}" ) ;
18031821
18041822 // UserPromptSubmit hook: `icm hook prompt` (recall context on each prompt)
18051823 let prompt_cmd = format ! ( "{} hook prompt" , icm_bin_str) ;
1806- let prompt_status =
1807- inject_claude_hook ( & claude_settings_path, "UserPromptSubmit" , & prompt_cmd) ?;
1824+ let prompt_status = inject_claude_hook (
1825+ & claude_settings_path,
1826+ "UserPromptSubmit" ,
1827+ & prompt_cmd,
1828+ None ,
1829+ & [ "icm hook" , "icm-post-tool" ] ,
1830+ ) ?;
18081831 println ! ( "[hook] Claude Code UserPromptSubmit (auto-recall): {prompt_status}" ) ;
18091832
18101833 // OpenCode plugin: install JS plugin for tool.execute.after + session.compacting
@@ -1859,10 +1882,14 @@ fn inject_icm_block(path: &PathBuf, block: &str) -> Result<String> {
18591882}
18601883
18611884/// Inject ICM hook into Claude Code settings.json for a given event name.
1885+ /// `matcher` is optional — if set (e.g. "Bash"), adds a matcher field to the hook entry.
1886+ /// `detect_patterns` lists substrings to detect if the hook is already present.
18621887fn inject_claude_hook (
18631888 settings_path : & PathBuf ,
18641889 event_name : & str ,
18651890 hook_command : & str ,
1891+ matcher : Option < & str > ,
1892+ detect_patterns : & [ & str ] ,
18661893) -> Result < String > {
18671894 let mut config: Value = if settings_path. exists ( ) {
18681895 let content = std:: fs:: read_to_string ( settings_path)
@@ -1898,7 +1925,7 @@ fn inject_claude_hook(
18981925 hooks. iter ( ) . any ( |h| {
18991926 h. get ( "command" )
19001927 . and_then ( |c| c. as_str ( ) )
1901- . map ( |c| c . contains ( "icm hook" ) | | c. contains ( "icm-post-tool" ) )
1928+ . map ( |c| detect_patterns . iter ( ) . any ( |p | c. contains ( p ) ) )
19021929 . unwrap_or ( false )
19031930 } )
19041931 } )
@@ -1910,76 +1937,19 @@ fn inject_claude_hook(
19101937 }
19111938
19121939 // Add ICM hook entry
1913- event_arr . push ( serde_json:: json!( {
1940+ let mut entry = serde_json:: json!( {
19141941 "hooks" : [ {
19151942 "type" : "command" ,
19161943 "command" : hook_command
19171944 } ]
1918- } ) ) ;
1919-
1920- let output = serde_json:: to_string_pretty ( & config) ?;
1921- std:: fs:: write ( settings_path, output)
1922- . with_context ( || format ! ( "cannot write {}" , settings_path. display( ) ) ) ?;
1923-
1924- Ok ( "configured" . into ( ) )
1925- }
1926-
1927- /// Inject ICM PreToolUse hook into Claude Code settings.json
1928- /// This hook auto-allows `icm` CLI commands (no permission prompt).
1929- fn inject_claude_pretool_hook ( settings_path : & PathBuf , hook_command : & str ) -> Result < String > {
1930- let mut config: Value = if settings_path. exists ( ) {
1931- let content = std:: fs:: read_to_string ( settings_path)
1932- . with_context ( || format ! ( "cannot read {}" , settings_path. display( ) ) ) ?;
1933- serde_json:: from_str ( & content)
1934- . with_context ( || format ! ( "cannot parse {}" , settings_path. display( ) ) ) ?
1935- } else {
1936- serde_json:: json!( { } )
1937- } ;
1938-
1939- let hooks = config
1940- . as_object_mut ( )
1941- . context ( "settings is not a JSON object" ) ?
1942- . entry ( "hooks" )
1943- . or_insert_with ( || serde_json:: json!( { } ) ) ;
1944-
1945- let pre_tool = hooks
1946- . as_object_mut ( )
1947- . context ( "hooks is not a JSON object" ) ?
1948- . entry ( "PreToolUse" )
1949- . or_insert_with ( || serde_json:: json!( [ ] ) ) ;
1950-
1951- let pre_tool_arr = pre_tool
1952- . as_array_mut ( )
1953- . context ( "PreToolUse is not an array" ) ?;
1954-
1955- // Check if ICM pretool hook already exists
1956- let already = pre_tool_arr. iter ( ) . any ( |entry| {
1957- entry
1958- . get ( "hooks" )
1959- . and_then ( |h| h. as_array ( ) )
1960- . map ( |hooks| {
1961- hooks. iter ( ) . any ( |h| {
1962- h. get ( "command" )
1963- . and_then ( |c| c. as_str ( ) )
1964- . map ( |c| c. contains ( "icm-pretool" ) || c. contains ( "icm hook pre" ) )
1965- . unwrap_or ( false )
1966- } )
1967- } )
1968- . unwrap_or ( false )
19691945 } ) ;
1970-
1971- if already {
1972- return Ok ( "already configured" . into ( ) ) ;
1946+ if let Some ( m) = matcher {
1947+ entry
1948+ . as_object_mut ( )
1949+ . unwrap ( )
1950+ . insert ( "matcher" . into ( ) , serde_json:: json!( m) ) ;
19731951 }
1974-
1975- // Add ICM PreToolUse hook entry (matcher: Bash — auto-allow icm commands)
1976- pre_tool_arr. push ( serde_json:: json!( {
1977- "matcher" : "Bash" ,
1978- "hooks" : [ {
1979- "type" : "command" ,
1980- "command" : hook_command
1981- } ]
1982- } ) ) ;
1952+ event_arr. push ( entry) ;
19831953
19841954 let output = serde_json:: to_string_pretty ( & config) ?;
19851955 std:: fs:: write ( settings_path, output)
@@ -3502,14 +3472,15 @@ fn cmd_memoir_list(store: &SqliteStore) -> Result<()> {
35023472 return Ok ( ( ) ) ;
35033473 }
35043474
3475+ let counts = store. batch_memoir_concept_counts ( ) . unwrap_or_default ( ) ;
35053476 println ! ( "{:<25} {:<8} Description" , "Name" , "Concepts" ) ;
35063477 println ! ( "{}" , "-" . repeat( 60 ) ) ;
35073478 for m in & memoirs {
3508- let stats = store . memoir_stats ( & m. id ) ? ;
3479+ let concept_count = counts . get ( & m. id ) . copied ( ) . unwrap_or ( 0 ) ;
35093480 println ! (
35103481 "{:<25} {:<8} {}" ,
35113482 m. name,
3512- stats . total_concepts ,
3483+ concept_count ,
35133484 truncate( & m. description, 40 )
35143485 ) ;
35153486 }
@@ -3746,11 +3717,8 @@ fn cmd_memoir_export(store: &SqliteStore, memoir_name: &str, format: &str) -> Re
37463717 let memoir = resolve_memoir ( store, memoir_name) ?;
37473718 let concepts = store. list_concepts ( & memoir. id ) ?;
37483719
3749- // Collect all outgoing links
3750- let mut links = Vec :: new ( ) ;
3751- for c in & concepts {
3752- links. extend ( store. get_links_from ( & c. id ) ?) ;
3753- }
3720+ // Batch load all links for this memoir (single query)
3721+ let links = store. get_links_for_memoir ( & memoir. id ) ?;
37543722
37553723 // Name lookup for links
37563724 let id_to_name: std:: collections:: HashMap < & str , & str > = concepts
0 commit comments